Skip to content

feat: MCP server for AI agent email access [WIP]#89

Draft
saiththerobo wants to merge 3 commits into
maathimself:mainfrom
saiththerobo:feat/mcp-server
Draft

feat: MCP server for AI agent email access [WIP]#89
saiththerobo wants to merge 3 commits into
maathimself:mainfrom
saiththerobo:feat/mcp-server

Conversation

@saiththerobo
Copy link
Copy Markdown
Collaborator

⚠️ Work in progress — not ready for merge. Opening as a draft to share the concept and get early feedback on the approach.

Summary

Adds an MCP (Model Context Protocol) server to mailflow, allowing any MCP-compatible AI assistant to read and manage a user's inbox through a secure Bearer-token-authenticated endpoint. This is an exploratory implementation to see what an MCP integration could look like for mailflow — it needs more testing and review before it would be production-ready.

Changes

  • backend/src/mcp/server.js — MCP server with 16 tools: list_accounts, list_folders, list_messages, get_message, get_thread, search_messages, mark_read, mark_starred, move_message, send_email, reply_to_message, forward_email, sync, get_unread_counts, delete_message, archive_message
  • backend/src/routes/mcp.js — StreamableHTTP endpoint at /api/mcp/stream; API key management routes (generate/revoke, session-authenticated)
  • backend/migrations/0008_mcp_api_key.sql — adds mcp_api_key column to users table
  • Service layer extraction — shared query and action logic pulled into services/messageQueries.js, services/mailActions.js, services/emailSend.js so both HTTP routes and MCP tools use the same functions with no duplicated SQL
  • Forward/reply HTMLgetMessage now fetches body_html; forwarded and replied messages include the original HTML body via quotedBodyHtml
  • Admin panel — "AI Agent" tab (robot-head icon) with endpoint URL display, API key generate/revoke, and a live-updating config snippet
  • i18nmcp tab label translated for all 6 supported locales
  • Tests — 215 vitest tests covering messageQueries, mailActions, emailSend, and mcp/server

New dependency: @modelcontextprotocol/sdk@^1.29.0 — the official MCP reference implementation. This is the only viable way to implement the StreamableHTTP transport and JSON-RPC protocol correctly. zod (already in the dependency tree as a peer dep of the SDK) is also used directly and should be declared explicitly — happy to add it if the approach is approved.

Testing

Manually tested against a live mailflow instance via MCP curl calls and a connected AI client:

  • Search, read, forward, reply, send, mark read/starred, move, delete, archive all verified working
  • Forward now correctly includes original HTML body (was broken before this PR — plain-text only)
  • API key generation and config snippet update verified in the UI
  • 215 automated tests pass (npm test)

More testing needed: edge cases around OAuth accounts, attachment forwarding, concurrent flag operations.


Contributor License Agreement

By submitting this pull request I confirm that:

  • I have read and agree to the Contributor License Agreement.
  • My contribution is my own original work (or I have identified any third-party material and confirmed it is compatible with the CLA).
  • I have the right to submit this contribution under the terms of the CLA.

- StreamableHTTP MCP endpoint at /api/mcp/stream (Bearer token auth)
- API key management routes (generate/revoke via session auth)
- Migration 0008: add mcp_api_key column to users table
- 16 tools: list_accounts, list_folders, list_messages, get_message,
  get_thread, search_messages, mark_read, mark_starred, move_message,
  send_email, reply_to_message, forward_email, sync, get_unread_counts,
  delete_message, archive_message
- All tools delegate to shared service layer (no SQL in MCP layer)
- forward/reply preserve original HTML body via quotedBodyHtml
- Admin panel: AI Agents tab (robot-head icon, id: mcp)
- Restore CONCURRENTLY in 0006/0007 migrations to origin versions
- 215 vitest tests covering messageQueries, mailActions, emailSend, mcp/server
@maathimself
Copy link
Copy Markdown
Owner

Current Blockers

1. Migration number conflict

The PR adds 0008_mcp_api_key.sql but main already has 0008_search_indexes.sql and 0009_thread_key_column.sql. The PR was authored against an older base. On any existing install this would either collide with or skip the search/threading migrations entirely. Needs a rebase onto current main and renumbering to the next available slot.

2. Unique constraint vulnerability in mailActions.js

deleteSingleMessage, moveSingleMessage, and archiveSingleMessage all use this pattern:

const newUid = await imapManager.moveMessage(...);
if (newUid != null) {
  await query('UPDATE messages SET folder = $1, uid = $2 WHERE id = $3', [...]);
}

There is a unique constraint on (account_id, uid, folder). If the destination folder already has a stale DB row at the server-assigned UID, this UPDATE crashes with a duplicate key violation. I just fixed the same bug in the bulk handlers in mail.js — the fix is to DELETE any stale row at (account_id, dest_folder, new_uid) before updating. The same fix needs to be applied to all three single-message functions here.

3. resolveTrashFolder reimplemented with a regression

mailActions.js reimplements trash folder resolution, but only matches %trash%. The canonical version in mailUtils.js also matches %deleted%, which is required for iCloud's "Deleted Items" and "Deleted Messages" folder names. The PR's version would silently fail to find the trash folder on iCloud, causing deletes to fall through incorrectly. Please use (or extend) the existing resolveTrashFolder from mailUtils.js rather than reimplementing it.

Should be addressed before merge

4. MCP API key stored in plaintext

All other sensitive values in the users and email_accounts tables (passwords, OAuth tokens, OIDC client secrets) are encrypted at rest via ENCRYPTION_KEY. The MCP key is written as a raw 64-char hex string. It should be either hashed (bcrypt — since it only needs to be verified, not retrieved) or encrypted consistently with the rest.

5. No rate limiting on /api/mcp/stream

Every other route in the backend has rate limiting. The MCP stream endpoint has none. An API key holder can call it in a tight loop and hammer the IMAP manager directly. Please add a rate limit consistent with the rest of the API.

6. folder_mappings ignored in mailActions.js

The existing resolveTrashFolder in mailUtils.js checks folder_mappings.trash first (user-configured override). The reimplemented version in this PR doesn't accept that parameter, so user folder mapping overrides are silently ignored for all MCP operations.

Fix the three blockers and the above items and I'll get this merged. Happy to discuss any of this, let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants