Skip to main content
Glama
MoorsArthur

Draftsafe MCP Server

by MoorsArthur
README.md
# Draftsafe for Thunderbird

Draftsafe lets an MCP client read local Thunderbird mail and request mailbox
changes. One Thunderbird add-on provides the loopback bridge, approval
window, Snooze, user-only Send later and Follow-ups. Agent-requested changes
need a Thunderbird click, or an active one-hour trust session that you started
with a Thunderbird click. Draft requests save to Drafts and never send. An
agent can also prepare an outgoing message in Thunderbird's native composer;
you must review it and click Send yourself every time.

**Security boundary:** MCP has no send or forward tool or HTTP route. The
combined add-on does have send permission for its user-only Send later feature
and includes a privileged Thunderbird Experiment. A compromised add-on could
send mail. Read the [security model](SECURITY.md) before relying on the route
boundary.
See the [privacy policy](PRIVACY.md) for local storage and network behavior.
Maintainers planning to publish source should follow the
[source publication gate](docs/source-publication.md).

## Understand Draftsafe in two minutes

For a clickable architecture map and guided tour, open the
[Understand Anything guide](docs/understand-anything.md). The graph is kept in
`.ua/knowledge-graph.json` and refreshed after verified code changes.

Draftsafe has two running parts: a small Node.js MCP server for your agent and
one Thunderbird add-on. The server and add-on talk over a private connection
on your computer. The agent can read messages through that connection, but
Thunderbird handles approvals and outgoing mail.

### The pieces

```mermaid
flowchart LR
  You[You] -->|ask for help| Agent[MCP client and agent]
  Agent -->|MCP tool call| Server[Draftsafe MCP server]
  Server -->|localhost and private token| Addon[One Draftsafe add-on]
  subgraph Thunderbird
    Addon -->|read or approved change| Mail[Accounts and messages]
    Addon -->|prepare a reply| Compose[Native compose window]
  end
  You -->|review or trust click| Addon
  You -->|Send click| Compose
```

The add-on contains the bridge, approval manager, Snooze, Send later and
Follow-ups. Its persistent background page keeps the bridge and scheduled
work available. The bridge accepts only fixed routes, and the MCP server
re-reads a private connection file after Thunderbird restarts. There is no
Draftsafe cloud service. Your MCP client may send message text to its model
provider when you ask the agent to work with that mail.

### What happens when you answer an email

```mermaid
sequenceDiagram
  actor You
  participant Agent as Agent
  participant MCP as Draftsafe MCP
  participant TB as Thunderbird
  You->>Agent: Help me answer this email
  Agent->>MCP: search_messages and get_message
  MCP->>TB: Read the selected message
  TB-->>MCP: Message data
  MCP-->>Agent: Message data to consider
  Agent->>MCP: open_compose_for_review
  MCP->>TB: Open reply with the account identity
  TB-->>MCP: Signature and original quote
  MCP->>TB: Insert escaped agent text above them
  TB-->>You: Show native compose window
  MCP-->>Agent: awaiting_user_send, sent: false
  You->>TB: Check From, To, subject and body
  You->>TB: Click Send when ready
```

You can edit or discard the reply. `awaiting_user_send` means a window opened;
it does **not** mean the email was sent. The agent has no send tool or send
route. Your Thunderbird Send click is required for each immediate message.
Thunderbird supplies the identity signature, so the agent body should not
include a sign-off.

### Who can do what?

| Task | What the agent can do | What you do in Thunderbird |
| --- | --- | --- |
| Find mail or a recipient | Search message data, Sent history and permitted local contacts | No approval needed for reads; you enable contacts in Thunderbird |
| Move, tag, mark read or unsubscribe | Request a bounded change | Review and click Allow, unless you started eligible one-hour trust |
| Save a draft or change a follow-up | Request the change | Review and click Allow, even during trust |
| Answer an email | Open, update or close agent-created composers, with attachments | Check the fields and click Send yourself |
| Send later | No MCP send-later route | Schedule it with Thunderbird's Send later button |

One-hour trust starts only with your Thunderbird click. It can skip the review
window for eligible mailbox changes for 60 minutes, but it cannot send, open
an outgoing message without your review, or approve drafts and follow-ups.

### What an approval changes

```mermaid
flowchart TD
  Request[Agent requests a mailbox change] --> Plan[Validate and preview the exact change]
  Plan --> Trust{Eligible one-hour trust active?}
  Trust -->|yes| Recheck[Recheck message and folder state]
  Trust -->|no| Window[Thunderbird review window]
  Window -->|Allow click| Recheck
  Window -->|Deny, close or expire| Stop[No change]
  Recheck --> Apply[Apply bounded change and record the result]
```

The approval window shows what will change. Closing it or leaving it for ten
minutes denies unfinished work. Mail can change while a request is pending,
so Draftsafe checks again just before applying it and reports partial results.

### If the connection stops working

Start with `check_connection`. If it cannot reach Thunderbird, check that
Thunderbird is running and the Draftsafe add-on is enabled, then restart the
MCP client. A **read timeout** means Thunderbird did not finish that read in
time; narrow the search by account, folder or date before retrying. A client
timeout during an approval does not cancel a change you already approved;
check approval history before asking the agent to repeat it.

## Requirements and build

- Thunderbird 140 ESR or newer. Background draft saving is available on 153+
  and is feature-detected. Linux, macOS and Windows connection paths are
  implemented; real Thunderbird smoke has only been run on Linux.
- Node.js 20 or newer for the MCP server.

```sh
npm ci
npm run build       # dist/index.js and dist/draftsafe.xpi
npm test            # unit, property and loopback tests
npm run typecheck
npm run smoke       # from a separate headless session only
```

`addons/app` contains the manifest and background entry point. `addons/bridge`
contains the socket Experiment and read routes. `addons/tools` contains the
approval manager and user features. `addons/shared` contains validators and
mail helpers. `mcp/src` contains the stdio server. The build packs one XPI.
The smoke runner refuses an interactive desktop session after focus switching
was reported during an Xvfb test run.

## Install

1. In Thunderbird, open **Tools → Add-ons and Themes → gear icon → Install
   Add-on From File…** and choose `dist/draftsafe.xpi`. Thunderbird allows
   add-ons with Experiment APIs, but its permission prompt correctly reports
   their broad authority.
2. Start or restart Thunderbird. In your MCP client, register the built
   server as a stdio command. For Claude Code:

   ```sh
   claude mcp add -s user draftsafe -- node /absolute/path/to/draftsafe/scripts/launch.mjs
   ```

   Generic MCP client configuration:

   ```json
   {
     "mcpServers": {
       "draftsafe": {
         "command": "node",
         "args": ["/absolute/path/to/draftsafe/scripts/launch.mjs"]
       }
     }
   }
   ```

3. Set the MCP client's tool timeout to at least **730 seconds** for approval
   requests. The client polls the request while the review window is open.
   A client timeout does not cancel an action already approved in Thunderbird;
   check approval history before retrying.
4. Call `check_connection`. It reports the add-on version and whether the
   approval manager is ready. Draftsafe checks bridge compatibility on each
   Thunderbird connection and reports `update_required` for an incompatible
   add-on. If a read times out, scope the next search by
   account, folder or date. A read timeout is distinct from disconnection.

### Updates

Draftsafe uses one `MAJOR.MINOR.PATCH` version across `package.json`, the
Thunderbird manifest and the MCP server. Each published version gets a Git tag
such as `v0.8.1` and a GitHub Release. `CHANGELOG.md` describes what changed.
The [source Releases](https://github.com/MoorsArthur/draftsafe/releases) and
[`CHANGELOG.md`](CHANGELOG.md) show version history. The
[public download Releases](https://github.com/MoorsArthur/draftsafe-updates/releases)
contain the XPI, MCP bundle, checksums and smoke report for each published
version. The separate download repository keeps the installed update URLs
stable.

The 0.8.1 XPI has a self-hosted update URL in the public distribution repository.
Existing 0.7.0 installations need one manual update to join that channel.
Later versions can be picked up by Thunderbird's normal add-on update checks.
The self-distributed add-on is not listed on addons.thunderbird.net. The public
XPI contains readable add-on code.
The MCP server is a separate Node process. It does not update when Thunderbird
updates an add-on.

`scripts/launch.mjs` is the stable MCP entry point. Its updater is **off by
default** unless the MCP client sets `DRAFTSAFE_AUTO_UPDATE=1`. The launcher
then uses the repository's pinned
[public key](updates/public-key.txt) and the public distribution repository's
stable signed manifest URL.
`DRAFTSAFE_UPDATE_URL` and `DRAFTSAFE_UPDATE_PUBLIC_KEY` can override those
defaults for an independently trusted feed.
The updater checks in a separate process, verifies the signed metadata and
bundle hash, installs lockfile-pinned dependencies without lifecycle scripts,
and uses the verified release on the **next** MCP start. A failed or offline
check leaves the current server running. The updater receives no bridge token
or mailbox data. `node scripts/launch.mjs --rollback` selects the previous
MCP release. `node scripts/launch.mjs --update-status` shows the last update
check outcome without exposing feed details or credentials. Do not configure
an unreviewed feed or key.

Release preparation: run `npm run release:check` in a headless session with
`THUNDERBIRD` pointing to a standalone binary. It runs tests, builds both
artifacts, checks an isolated Thunderbird smoke report, and builds the MCP
update bundle and a Thunderbird update manifest. `build:update` writes a reviewable JSON bundle and its SHA-256; it does
not sign, publish or install it. A release signer must sign the exact JSON
object `{schema,version,bundleUrl,sha256}` in that property order with Ed25519
and add a base64 `signature` field. `npm run sign:update` does this after the
release operator supplies `DRAFTSAFE_RELEASE_BUNDLE_URL` and a private key
file path via `DRAFTSAFE_RELEASE_KEY_FILE`. Keep the key only in
`~/.config/secrets` and out of logs and the repository. The XPI and MCP bundle
are published together in the public distribution repository. The exact
publication procedure is in [release channels](docs/release-channels.md).

### Upgrade from the two-add-on release

The release add-on uses `draftsafe-tools@armain.be`, the ID of the installed
personal Tools add-on, so its Snooze, Send later, Follow-up and approval
history storage persists. Disable the old
**Draftsafe Bridge** add-on first. Then install `dist/draftsafe.xpi` as an
update to **Draftsafe Tools**, restart Thunderbird and the MCP client, and
call `check_connection`. The old Bridge and the combined add-on must not run
together: both would publish the same connection file. After verifying the
new connection, remove the disabled Bridge manually if desired.

Earlier private builds using `draftsafe-tools@draftsafe.dev` have separate
Thunderbird storage. Export or migrate their state before switching IDs.

### Connection file

| Thunderbird install | Connection file |
| --- | --- |
| Snap | `~/snap/thunderbird/common/draftsafe-mcp/connection.json` |
| deb, tarball, distro package | `${XDG_STATE_HOME:-~/.local/state}/draftsafe-mcp/connection.json` |
| Flatpak | `~/.var/app/org.mozilla.Thunderbird/.local/state/draftsafe-mcp/connection.json` |
| macOS | `~/Library/Application Support/draftsafe-mcp/connection.json` |
| Windows | `%LOCALAPPDATA%\draftsafe-mcp\connection.json` |

The file contains a version, random port and new bearer token on each start.
On Linux, MCP checks the newest supported candidate and re-reads it on every
call. Set `DRAFTSAFE_CONNECTION_FILE` to override discovery. Keep this file
private. The Snap common path survives Snap revision changes. Multiple
Thunderbird profiles sharing a connection path remain unsupported; the newest
publisher wins.

## MCP tools

| Tool | Purpose | Changes mail? |
| --- | --- | --- |
| `check_connection` | Check add-on and approval readiness | no |
| `list_accounts` | Accounts, identities, folders and tags | no |
| `list_folders_detailed` | Folder IDs, tree, special use and counts | no |
| `search_messages` | Filtered, paginated search | no |
| `find_recipients` | Suggest addresses from bounded Sent history and optional local contacts | no |
| `get_message` | Headers, plain-text body and attachment metadata | no |
| `get_thread` | Conversation linked by message headers | no |
| `list_followups` | Messages tagged Follow up | no |
| `request_cleanup` / `request_trash` | Recoverable Trash, Archive or ordinary-folder move | approval |
| `request_folder_changes` | Create, rename, merge or move empty folder to Trash | approval |
| `request_unsubscribe` | Header-derived HTTPS one-click POST | approval |
| `set_tags`, `mark_read`, `set_followup` | Tag, read flag or follow-up | approval |
| `create_draft` | Save new or reply draft, never send | approval |
| `open_compose_for_review` | Open a prefilled native composer for your review | opens a window; only your Send click sends |
| `open_composes_for_review` | Prepare 2 to 20 distinct messages in one request | opens separate windows; only your Send clicks send |
| `list_open_composes_for_review` | Find the tab ID and edit status of open agent-prepared composers | no |
| `update_compose_for_review` | Edit agent text, new-mail fields and agent-added attachments | updates a window; only your Send click sends |
| `close_compose_for_review` | Close an unchanged agent-prepared composer | Thunderbird handles any unsaved-draft prompt; never sends |

`search_messages` can return fewer results than `limit` on any page. Pass
`nextCursor` back until `complete:true`; an empty incomplete page is not a
“not found” result. Use `fast:true` for a quick first page without extra
classification-header reads, then `get_message` for full headers before a
newsletter or unsubscribe action. Sender and recipient names match partially;
email addresses in `from` and `to` must be complete. Results are not globally
sorted by date. Numeric message IDs are session-scoped and
can change after a move or Thunderbird restart. Search again before acting
on old IDs. A completed empty result means no matches in that query. A `busy`
or `timeout` error means the search did not finish: narrow it by account, folder, sender,
subject or date and retry. Body-text `query` can take longer across a large
mailbox. The bridge keeps health checks available during slow searches.
`find_recipients` searches local contacts only after you click **Enable local
contacts** in Draftsafe's Follow-ups popup. Without that permission, it uses
at most three pages of Sent mail from the last year. It reports ambiguity and
truncation; confirm the address before using it in a composer.
`list_folders_detailed` does not enumerate mail to calculate
date extrema, so `oldest` and `newest` are `null`.

### Prepare an email for review

`open_compose_for_review` takes plain-text input and opens Thunderbird's normal
compose window in the selected identity's format. Thunderbird adds the
configured identity signature and, for replies, its own quoted original. Do
not include a sign-off in the agent body. For a new message, provide `to`,
`subject` and `body`:

```json
{"to":["recipient@example.test"],"subject":"Meeting notes","body":"Hello,\n\nHere are the notes."}
```

For a threaded reply, provide `reply_to_message_id` and `body` instead.
Thunderbird fills the reply recipient, subject and sending identity from the
message's account. The agent text is inserted above Thunderbird's signature
and quote; HTML identities keep their formatting and inline logo. Review
**From**, **To**, subject, body and attachments before using Thunderbird's
Send button. You can edit or discard the message. The MCP result is
`awaiting_user_send` with `sent:false`; it never confirms delivery. Active
one-hour trust cannot skip this review. There is no hourly compose quota or
fixed limit on simultaneous agent-prepared composers. The result includes
`tabId`, which identifies the window for `update_compose_for_review`. When
asking the agent to revise a message, it should call
`list_open_composes_for_review`, then `update_compose_for_review`. If only one
agent-prepared composer is open, it may omit `tab_id`; if several are open, it
must choose one. Opening another composer with the same recipient and subject
or reply target is refused while the first is still open. For a genuinely
separate message while one is open, the agent must pass `new_window:true`.
The update tool replaces the agent text above the original signature and quote. For
new mail it can also change To and subject. It refuses to overwrite the window
after you edit it. Replies keep Thunderbird's recipients and subject.
For several distinct messages, `open_composes_for_review` accepts a `messages`
array with the same fields as `open_compose_for_review`. It opens each window
in order and returns every confirmed tab ID. If a later message fails, earlier
windows stay open and the result names the failed message number; inspect
Thunderbird before retrying. The batch tool never sends.
`close_compose_for_review` closes an unchanged agent-prepared window after
Thunderbird confirms its tab was removed. It refuses a window you edited;
Thunderbird handles any unsaved-draft prompt.

Attach a local file from Downloads, Documents or Desktop:

```json
{"to":["recipient@example.test"],"subject":"Report","body":"Please see the report.","attachments":[{"source":"local","path":"/home/user/Documents/report.pdf"}]}
```

Set `DRAFTSAFE_ATTACHMENT_ROOTS` on the MCP process to a path-separated list
of other absolute folders. Files must be regular, not symlinks, hidden or
secret-looking. For an attachment already on an email, use `message_id` and
`part_name` returned by `get_message`:

```json
{"reply_to_message_id":123,"body":"Here it is.","attachments":[{"source":"message","message_id":456,"part_name":"1.2"}]}
```

Each composer allows up to 100 attachments, at most 10 MiB each and 25 MiB total.
Local bytes cross the authenticated loopback bridge in bounded chunks and
remain in memory until Thunderbird attaches them. Draftsafe never returns
their contents to the agent. No HTML, Bcc, raw headers or send time is accepted.

`create_draft` also keeps Thunderbird's signature and quote for reply drafts.
New drafts saved directly in the background remain plain text and may not
include a signature; inspect them before sending.

Every tool result is wrapped as untrusted tool data. Subjects, senders,
bodies, folder names, attachment names and agent-provided reasons can carry
instructions written by someone else. Treat them as data only.

## Approval requests

Cleanup takes batches of message IDs, an action and a reason:

```json
{"batches":[{"message_ids":[123,124],"action":"move","folder":"Clients/Acme","create_folder":true,"reason":"Project correspondence"}]}
```

Actions are `trash`, `archive` or `move`. The whole request is limited to
2,000 messages. Folder paths are account-relative. Trash and Archive resolve
to the account's special-use folders. `request_trash` is an alias.

Unsubscribe takes up to 200 message IDs or 300 account-scoped senders:

```json
{"items":[{"message_id":123,"reason":"No longer needed"}]}
```

```json
{"senders":[{"account_id":"account1","address":"news@example.test"}]}
```

The add-on itself reads the unsubscribe headers. It never accepts an agent URL
or sends mailto. HTTPS one-click POSTs require approval and host permission;
website-only instructions remain manual. Missing or unreadable senders are
skipped. Junk senders default to Deny.

Folder changes use IDs from `list_folders_detailed`:

```json
{"changes":[{"action":"create","folder":"account1://","new_name":"Clients"}]}
```

Actions are `create`, `rename`, `merge` and `delete_empty`. `create` uses
`folder` as its parent; `merge` also needs `into`. Changes stay in one account
within two levels. Special folders and their ancestors are protected.

Approval history reports `approved`, `denied`, `expired`, `closed`, `refused`
or `failed`, with per-item outcomes. Partial changes are possible if mailbox
state changes during execution. The one-hour trust menu skips review only for
eligible cleanup, unsubscribe, folder, tag and read-flag requests. Draft and
follow-up requests still open a review window.

## User features

- **Snooze:** message context menu or message-view button. A message moves to
  an ordinary account-local Snoozed folder and later returns to the special-use
  Inbox. Unsafe source and destination folders are refused.
- **Send later:** compose-window button. It saves a draft and schedules it.
  At due time, the draft must still match its recorded Message-ID, folder,
  recipients and content fingerprint. Cancellation and claiming are serialized.
  Changed or ambiguous drafts fail closed. Sends more than 12 hours late and
  interrupted sends are not retried.
- **Follow-ups:** context menu and toolbar list with overdue badge. The agent
  can request setting or clearing the tag; due dates stay user-only.

## Smoke test and limitations

`npm run smoke` starts Thunderbird in a throwaway profile under Xvfb, installs
the single add-on plus a test seeder, and drives the real MCP server. It checks
reads, drafts, approval clicks, denied changes, moves, folder changes,
transport and granted permissions. Synthetic clicks must fail. An SMTP trap
must see no connection from agent calls, then exactly one after a native Send
click on the isolated Thunderbird display. The runner refuses an existing
Draftsafe connection file and never touches the real Thunderbird profile or
display. The 0.8.1 release passed 58 isolated Thunderbird checks. Its
combined add-on was also verified live in the maintainer's Thunderbird before
publication. New releases still need isolated smoke and review before their
update manifests are published.

Thunderbird's API gives no transaction or conditional move, so concurrent
mailbox changes can cause a partial result. Search and thread lookup may be
incomplete on very large or unusual accounts. A stalled Thunderbird call can
continue after the HTTP read deadline; Draftsafe keeps its in-flight permit
until that call finishes. See [SECURITY.md](SECURITY.md) for the full threat
model and [CONTRIBUTING.md](CONTRIBUTING.md) for development rules.

Draftsafe is MIT-licensed. "Thunderbird" is a Mozilla trademark; this project
is not affiliated with or endorsed by Mozilla.