Architecture

Architecture

MailAfrica Agent is a dual-process Python service that bridges MailAfrica’s email platform with AI assistants via MCP and provides autonomous auto-reply capabilities.

Two Processes

ProcessTransportPurposeWhere
MCP ServerstdioTool interface for AI assistantsLocal machine
Webhook ServerHTTP (port 8097)Receive inbound email, serve chat APIVPS / Docker

Both processes share the same SQLite database (agent.db) safely using WAL (Write-Ahead Logging) mode with a 5-second busy timeout.

Module Breakdown

mcp_server.py

  • Defines 20 MCP tools using @mcp.tool() decorators
  • Uses FastMCP from the mcp SDK
  • Tools call into MailAfricaClient, NgamiaClient, Agent, and Store
  • Runtime is initialized via a lifespan context manager

webhook.py

  • FastAPI application with 3 endpoints
  • Webhook endpoint verifies HMAC signatures, then spawns Agent.handle_message() as a background task
  • Chat endpoint wraps Agent.chat() for HTTP access
  • Returns immediately with {"status": "queued"} for fast webhook acks

agent.py

  • Agent class orchestrates the auto-reply pipeline
  • Safety gate: drops bounces, auto-responders, bulk mail
  • Loads per-address config from MailAfrica’s database
  • Builds LLM prompts with conversation history (last 40 turns)
  • chat() method supports OpenAI function-calling with 3 tools

mailafrica.py

  • Async HTTP client wrapping the MailAfrica REST API
  • All methods follow method() -> data pattern, unwrapping the {success, data, errors} envelope
  • Raises MailAfricaError on API failures

ngamia.py

  • Wraps the OpenAI SDK pointed at Ngamia’s gateway
  • Provides complete(), complete_with_tools(), and list_models()
  • Uses AsyncOpenAI for non-blocking calls

store.py

  • SQLite via aiosqlite with WAL mode
  • Single conversations table with thread key indexing
  • Thread key = sender:::normalized_subject
  • Thread-safe for multi-process access

config.py

  • Pydantic Settings class loading from environment / .env
  • All configuration in one place with sensible defaults

Data Flow: Auto-Reply

Inbound Email
     |
     v
MailAfrica API --webhook--> Webhook Server
                                |
                                v
                          Verify HMAC
                                |
                                v
                          Queue background task
                                |
                                v
                    Fetch full message from API
                                |
                                v
                    Safety gate (bounces, auto-responders)
                                |
                                v
                    Load per-address config
                                |
                                v
                    Append to conversation thread
                                |
                                v
                    Build LLM prompt (persona + history)
                                |
                                v
                    Call Ngamia LLM
                                |
                                v
                    Record reply in thread
                                |
                                v
                    Send (auto) or save (draft)

Data Flow: MCP Tool Call

AI Assistant (Claude, Cursor, etc.)
     |
     v
MCP stdio transport
     |
     v
FastMCP server
     |
     v
Tool function (e.g., send_email)
     |
     v
MailAfricaClient --> MailAfrica API
     |
     v
Response returned to AI assistant

Database Schema

Single table with thread-based indexing:

CREATE TABLE IF NOT EXISTS conversations (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    thread_key  TEXT NOT NULL,
    role        TEXT NOT NULL,       -- 'user' or 'assistant'
    content     TEXT NOT NULL,
    message_id  INTEGER NOT NULL,
    created_at  TEXT NOT NULL
);
 
CREATE INDEX IF NOT EXISTS idx_conversations_thread
    ON conversations (thread_key, created_at);

Thread key formula:

normalized = re.sub(r"^\s*(re|fw|fwd)\s*:\s*", "", subject, flags=re.IGNORECASE)
thread_key = f"{sender.lower()}:::{normalized or '(no subject)'}"

Authentication Boundaries

CredentialEnv VarPurpose
MailAfrica API KeyMAILAFRICA_API_KEYX-API-Key header on every MailAfrica call
Webhook SecretAGENT_WEBHOOK_SECRETHMAC-SHA256 verification of webhook deliveries
Ngamia API KeyNGAMIA_API_KEYAuthorization: Bearer for LLM calls

Deployment Architecture

ComponentTechnologyLocation
MailAfrica APIGo APIapi.mailafrica.online
Ngamia LLM GatewayOpenAI-compatibleapi.ngamia.cc/v1
MailAfrica AgentPythonagent.mailafrica.online:8097

Design Decisions

Why stdio for MCP?

The MCP server uses stdio transport because:

  • No open ports = no attack surface
  • The AI assistant that spawns the process is the only client
  • Works locally without network configuration
  • Standard MCP transport supported by all major clients

Why SQLite?

  • Zero configuration — no database server to manage
  • WAL mode allows concurrent reads from both processes
  • Conversation history is local and fast
  • Easy to back up (single file)

Why Background Tasks for Webhooks?

Webhook delivery must return quickly (MailAfrica expects a fast 2xx). The agent queues the auto-reply as a background asyncio.Task and returns immediately, then processes the message asynchronously.