bichon-mcp
# bichon-mcp
MCP server that wraps the [Bichon](https://github.com/rustmailer/bichon) email archiver REST API, letting an AI agent (Claude Code, Claude Desktop) search and read locally-archived emails — without ever touching Gmail or IMAP credentials directly.
## Architecture
```
Gmail / IMAP
└─► Bichon (Docker, localhost:15630)
local Tantivy FTS + compressed EML store
└─► bichon-mcp (this repo, stdio MCP server)
└─► Claude Code / Claude Desktop
```
After the initial IMAP sync, the AI only ever calls `localhost:15630`. No external auth is granted to the AI.
## Requirements
- Python 3.11+
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
- Bichon running locally — see [Bichon quickstart](https://github.com/rustmailer/bichon)
## Installation
### 1. Clone and install
```bash
git clone https://github.com/pras-labs/bichon-mcp
cd bichon-mcp
uv venv
uv pip install -e .
```
### 2. Get a Bichon access token
Start Bichon (Docker):
```bash
docker run -d --name bichon -p 15630:15630 rustmailer/bichon:latest
```
Then get a token:
```bash
curl -s -X POST http://localhost:15630/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin@bichon"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])"
```
For a long-lived token, create one in the Bichon WebUI under **Settings → Access Tokens**.
### 3. Configure your client
#### Claude Code
Copy `.mcp.json.example` to `.mcp.json` and fill in the token:
```bash
cp .mcp.json.example .mcp.json
```
```json
{
"mcpServers": {
"bichon-email": {
"command": ".venv/bin/python",
"args": ["-m", "bichon_mcp"],
"env": {
"BICHON_BASE_URL": "http://localhost:15630",
"BICHON_ACCESS_TOKEN": "<your-token>"
}
}
}
}
```
> `.mcp.json` is gitignored — your token will not be committed.
#### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"bichon-email": {
"command": "/path/to/bichon-mcp/.venv/bin/python",
"args": ["-m", "bichon_mcp"],
"env": {
"BICHON_BASE_URL": "http://localhost:15630",
"BICHON_ACCESS_TOKEN": "<your-token>"
}
}
}
}
```
## Configuration
All configuration is via environment variables:
| Variable | Default | Description |
|---|---|---|
| `BICHON_BASE_URL` | `http://localhost:15630` | Bichon instance URL |
| `BICHON_ACCESS_TOKEN` | *(required)* | Bearer token for Bichon API |
| `BICHON_STATS_CACHE_TTL` | `300` | Mailbox stats cache TTL in seconds. `0` = disabled |
| `BICHON_REQUEST_TIMEOUT` | `30` | HTTP request timeout in seconds |
## MCP Tools
### `list_accounts()`
List all email accounts configured in Bichon.
```
Returns: [{id, email}]
```
**Example prompt:** *"What email accounts are set up?"*
---
### `list_mailboxes(account_id)`
List mailbox folders for an account (INBOX, Sent, Drafts, etc.).
```
account_id* integer From list_accounts
Returns: [{id, name, total, unseen}]
```
**Example prompt:** *"Show me all mailboxes for my account."*
---
### `get_mailbox_stats(account_id, mailbox_id)`
Per-mailbox statistics: total email count, top senders, top subjects by frequency. Results are cached (default 5 min, configurable via `BICHON_STATS_CACHE_TTL`).
```
account_id* integer From list_accounts
mailbox_id* integer From list_mailboxes
Returns:
total_emails integer Exact count from Bichon
sampled integer Emails analysed (up to 200 most recent)
top_senders [{sender, count}]
top_subjects [{subject, count}]
oldest_in_sample / newest_in_sample integer (unix ms)
cached boolean
```
**Example prompt:** *"Give me stats for my INBOX — who emails me most?"*
---
### `search_emails(query, ...)`
Full-text search across archived emails. Returns summaries only — no body text.
```
query* string Full-text search term (max 500 chars)
date_from string ISO date YYYY-MM-DD (inclusive)
date_to string ISO date YYYY-MM-DD (inclusive)
sender string Filter by sender address (max 254 chars)
account_id integer Scope to one account
mailbox_id integer Scope to one mailbox folder
limit integer Max results, default 20, max 50
Returns: [{id, account_id, subject, from, to, date, preview, thread_id, has_attachments}]
```
**Example prompts:**
- *"Search for emails about invoices from last month."*
- *"Find emails from notifications@github.com in my INBOX."*
---
### `get_email(account_id, envelope_id)`
Fetch the full plain-text content of one email. HTML is stripped. Body is truncated at ~4000 tokens.
```
account_id* integer From search_emails result
envelope_id* string UUID from search_emails result
Returns: {account_id, envelope_id, body, attachment_count}
```
**Example prompt:** *"Show me the full content of that invoice email."*
---
### `list_threads(subject_contains?, sender?, limit?)`
Group search results by `thread_id` to show conversation threads.
```
subject_contains string Filter by subject text (max 500 chars)
sender string Filter by sender (max 254 chars)
limit integer Max threads to return, default 10
Returns: [{thread_id, account_id, subject, latest_from, latest_date, count}]
```
**Example prompt:** *"Show me all threads from GitHub notifications."*
---
### `get_sender_summary(email_address)`
Aggregate stats for a sender: total email count, date range, top subjects.
```
email_address* string Valid email address (max 254 chars)
Returns: {email, count, first_seen, last_seen, top_subjects}
```
**Example prompt:** *"How many emails have I received from packt@mail.packtpub.com and what are they about?"*
---
## Typical workflow
```
User: "Summarise my email activity for the past month."
Claude:
1. list_accounts() → finds account IDs
2. list_mailboxes(account_id) → finds INBOX mailbox_id
3. get_mailbox_stats(...) → top senders, subjects, total count
4. search_emails(query="", date_from="2026-04-20", ...) → recent emails
5. get_email(...) × N → reads relevant messages
→ synthesises a summary
```
## Security notes
- The access token is read from `BICHON_ACCESS_TOKEN` at startup — never hardcode it in source files.
- `.mcp.json` is gitignored to prevent accidental token commits.
- `envelope_id` values are validated as UUIDs before use in API paths.
- All string inputs have length limits; integer IDs must be positive.
- API error responses are not forwarded to the AI (only the HTTP status code is).
- Email bodies are passed directly to Claude. Malicious emails may contain text designed to manipulate AI behaviour — a known limitation of any email-reading AI tool.
## Development
```bash
# Install in editable mode
uv pip install -e .
# Verify MCP tools are registered (no Bichon needed)
python3 -c "
import subprocess, json, os, threading, queue, time
proc = subprocess.Popen(['.venv/bin/python', '-m', 'bichon_mcp'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
env={**os.environ, 'BICHON_BASE_URL':'http://localhost:15630','BICHON_ACCESS_TOKEN':'test'},
text=True, bufsize=1)
q = queue.Queue()
threading.Thread(target=lambda: [q.put(l.strip()) for l in proc.stdout], daemon=True).start()
for msg in [
{'jsonrpc':'2.0','id':1,'method':'initialize','params':{'protocolVersion':'2024-11-05','capabilities':{},'clientInfo':{'name':'t','version':'0'}}},
{'jsonrpc':'2.0','method':'notifications/initialized','params':{}},
{'jsonrpc':'2.0','id':2,'method':'tools/list','params':{}},
]:
proc.stdin.write(json.dumps(msg)+'\n')
proc.stdin.flush()
deadline = time.time()+10
while time.time()<deadline:
try:
line = q.get(timeout=1)
obj = json.loads(line)
if obj.get('id')==2:
print([t['name'] for t in obj['result']['tools']])
break
except: pass
proc.terminate()
"
# Expected: ['list_accounts', 'list_mailboxes', 'get_mailbox_stats', 'search_emails', 'get_email', 'list_threads', 'get_sender_summary']
```
## License
AGPL-3.0. If you distribute this software, the source must remain open.
TDQS
Scored across 7 tools
Each tool targets a distinct purpose: listing accounts, mailboxes, threads; searching emails; retrieving full email content; and obtaining statistics on a mailbox or sender. No overlapping functionality.
All tools follow a consistent verb_noun pattern using snake_case: 'list_' for enumerations, 'get_' for retrievals, and 'search_' for searching. No mixing of conventions.
With 7 tools, the set is well-scoped for a read-only email retrieval and statistics server. Each tool covers a necessary operation without redundancy or deficiency.
The tool surface covers the full lifecycle for read-only email access: listing accounts and mailboxes, searching, retrieving full content, and aggregating stats. No obvious gaps for the stated purpose.