Full email, calendar & contacts via JMAP. The wrapper exposes 13 MCP tools so AI agents can search, read, send, and manage your mailbox — all through a single unified API.
13 tools exposed to AI agents via the Kiwi MCP gateway. Grouped by domain.
How the wrapper sits in front of Stalwart inside the LXC container.
Stalwart is a full-featured mail server licensed under AGPL-3.0. The KiwiStack wrapper
communicates with it exclusively over HTTP (JMAP protocol on 127.0.0.1:8080), maintaining a strict
network boundary between the AGPL-licensed upstream and the Apache-2.0 wrapper.
Because the wrapper never links against Stalwart’s code — it only speaks HTTP to it — the AGPL copyleft does not propagate to the wrapper or any other KiwiStack component. Stalwart runs as an unmodified binary from the upstream Alpine package.
The wrapper handles: JWT validation (tokens from Kiwi ID), JMAP translation (converting Kiwi API calls into JMAP method calls), health checking (polling Stalwart’s health endpoint with exponential backoff), and MCP tool registration (advertising the 13 tools to Kiwi MCP).
┌──────────────────────────────────┐ │ LXC: kiwi-mail │ │ │ │ Stalwart: 127.0.0.1:8080 │ ← AGPL-3.0 │ ↓ localhost HTTP (JMAP) │ │ Wrapper: 10.10.20.11:8443 │ ← Apache-2.0 │ │ │ Only wrapper is on the network │ └──────────────────────────────────┘
Wrapper configuration — lives at /etc/kiwi/config.toml.
# kiwi-mail wrapper configuration listen_addr = "10.10.20.11:8443" upstream_addr = "127.0.0.1:8080" upstream_bin = "/opt/upstream/bin/stalwart-mail" upstream_config = "/opt/upstream/etc/config.toml" health_check_interval = "5s" health_check_timeout = "60s" jwt_public_key = "/etc/kiwi/jwt-public.pem" log_level = "info"
| Key | Type | Description |
|---|---|---|
| listen_addr | string | Bind address for the wrapper (private bridge) |
| upstream_addr | string | Stalwart JMAP endpoint on localhost |
| upstream_bin | path | Path to the upstream Stalwart binary |
| upstream_config | path | Path to Stalwart’s own config file |
| health_check_interval | duration | How often the wrapper polls Stalwart’s health endpoint |
| health_check_timeout | duration | Max wait for Stalwart to become healthy at startup |
| jwt_public_key | path | Kiwi ID public key for JWT validation |
| log_level | string | Tracing level: error, warn, info, debug, trace |
How files are organized inside the LXC container.
/ ├── opt/ │ ├── upstream/ # Stalwart (AGPL-3.0) │ │ ├── bin/stalwart-mail # unmodified binary from Alpine pkg │ │ ├── etc/config.toml # Stalwart config (JMAP port, storage) │ │ └── lib/ # shared libs (rocksdb, etc.) │ └── kiwi/ # Kiwi wrapper (Apache 2.0) │ ├── bin/kiwi-mail # wrapper binary (static musl build) │ └── version # "0.3.1" ├── etc/ │ └── kiwi/ │ ├── config.toml # wrapper config (see above) │ └── jwt-public.pem # Kiwi ID public key ├── var/ │ ├── lib/ │ │ └── upstream/ # Stalwart data │ │ ├── db/ # RocksDB / SQLite databases │ │ ├── blobs/ # email blobs (or S3 via kiwi-store) │ │ └── queue/ # SMTP delivery queue │ └── log/ │ └── kiwi/ # JSON structured logs └── run/
Understanding the AGPL boundary.
Stalwart (upstream) is licensed under AGPL-3.0 — the strongest copyleft license. If you modify Stalwart and serve it over a network, you must share your modifications. However, KiwiStack does not modify Stalwart. It runs the unmodified binary and communicates via HTTP.
The Kiwi Mail wrapper is licensed under Apache-2.0. Because the wrapper only talks to Stalwart over a network socket (JMAP over HTTP), the AGPL copyleft does not propagate. This is the same principle that allows any web browser to talk to an AGPL-licensed server without becoming AGPL itself.
Rule: Never link AGPL code. Never import AGPL crates. Only communicate over the network boundary.
An MCP tools/call for mail.search, and the response.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "mail.search",
"arguments": {
"query": "quarterly report",
"after": "2026-01-01",
"limit": 5
}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{
"type": "text",
"text": "Found 3 emails matching 'quarterly report':\n\n1. [Feb 14] From: alice@example.com — Q4 Quarterly Report\n2. [Jan 28] From: bob@example.com — RE: Quarterly Report Draft\n3. [Jan 15] From: carol@example.com — Quarterly Report Template"
}],
"isError": false
}
}
Pinned versions — from compatibility.toml.
# kiwi-mail/compatibility.toml [upstream] name = "stalwart" version = "0.10.5" checksum = "sha256:..." [wrapper] version = "0.1.0" rust_edition = "2024" msrv = "1.85" [tested] date = "2026-02-28" kiwi_id = "0.1.0"