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
| Process | Transport | Purpose | Where |
|---|---|---|---|
| MCP Server | stdio | Tool interface for AI assistants | Local machine |
| Webhook Server | HTTP (port 8097) | Receive inbound email, serve chat API | VPS / 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
FastMCPfrom themcpSDK - Tools call into
MailAfricaClient,NgamiaClient,Agent, andStore - 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
Agentclass 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() -> datapattern, unwrapping the{success, data, errors}envelope - Raises
MailAfricaErroron API failures
ngamia.py
- Wraps the OpenAI SDK pointed at Ngamia’s gateway
- Provides
complete(),complete_with_tools(), andlist_models() - Uses
AsyncOpenAIfor non-blocking calls
store.py
- SQLite via
aiosqlitewith WAL mode - Single
conversationstable 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 assistantDatabase 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
| Credential | Env Var | Purpose |
|---|---|---|
| MailAfrica API Key | MAILAFRICA_API_KEY | X-API-Key header on every MailAfrica call |
| Webhook Secret | AGENT_WEBHOOK_SECRET | HMAC-SHA256 verification of webhook deliveries |
| Ngamia API Key | NGAMIA_API_KEY | Authorization: Bearer for LLM calls |
Deployment Architecture
| Component | Technology | Location |
|---|---|---|
| MailAfrica API | Go API | api.mailafrica.online |
| Ngamia LLM Gateway | OpenAI-compatible | api.ngamia.cc/v1 |
| MailAfrica Agent | Python | agent.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.