email-thread-mcp
MCP server (stdio) with two deterministic tools for an accounts-receivable (AR) email agent: no LLM inside, no network access, no credentials.
| Tool | Role in the agent | Output contract |
|---|---|---|
parse_email_thread(body, ar_mailboxes?) |
first step: request preprocessing. Splits the raw email body (Outlook/Gmail/Apple HTML or plain text) into the latest message, its signer and greeting, and the quoted history (oldest first) | contracts/thread.schema.json |
validate_output(output, categories, thread?) |
last step: deterministic checks of the final envelope. Corrects only unambiguous problems (category case/punctuation, duplicates, invoice-number noise, human-review flag); reports the rest | contracts/validation.schema.json |
Use
Any MCP client that starts stdio servers can run it with uvx:
{
"mcpServers": {
"email-thread-mcp": {
"command": "uvx",
"args": ["email-thread-mcp@0.2.0"]
}
}
}
Pin a version: a client that keeps the server running may otherwise serve a stale cached build.
Each tool returns one JSON object. Structured arguments (ar_mailboxes, categories, output,
thread) may be passed as values or as text: clients that render arguments through a template
engine first can send JSON or a Python-style repr, and both are accepted.
A typical agent calls parse_email_thread first, as preprocessing, and uses latest_message,
reply_to_name and thread_context in later steps; it calls validate_output last, on its final
envelope. If parsing fails, falling back to the plain-text body before the first quote marker keeps
the agent working.
Language support
Inbound mail may be in any language, so the parser treats its three kinds of miss differently:
| Miss | Effect | How it is avoided |
|---|---|---|
| quote boundary | old history read as the new message | found by structure in any language: the mail client's HTML quote marker (Outlook divRplyFwdMsg, Gmail gmail_quote, Apple/Thunderbird blockquote type="cite"), or a label: value block that starts with an address and holds a date with a time |
| greeting / signer name | reply_to_name is null, the draft greets neutrally |
per-language words in src/email_thread_mcp/vocab.py |
| sent date | sent_at is null (thread order comes from position, not dates) |
dateparser, absolute dates only |
vocab.py covers EN, DE, FR, NL, IT, ES, PT, PL, CS, SV, DA, NO, FI. Only EN is verified against
real threads; the others are built from Outlook's localized reply headers. A language outside the
list still splits, and parse_info.warnings names what was not understood, e.g.
quoted message 1: header labels not recognised: Kimden, Gönderildi, Kime, Konu. Search traces
for those warnings to see which language to add next.
To add or verify a language: add its entry to vocab.py and a case to tests/test_languages.py.
Once a real thread in it (anonymised as below) parses correctly, add it as a fixture and set verified.
Untrusted input
The body is written by the customer, and anyone can send one. The parser:
- drops text a reader cannot see (CSS
display:none,visibility:hidden,font-size:0,opacity:0,mso-hide:all, thehiddenattribute) and zero-width, bidirectional and control characters, and reports both inparse_info.warnings; - bounds its own cost: the body is cut at 2M raw / 300k text characters, and dates beyond 50 parser calls per email stay null.
It does not make the content trustworthy. The quoted history in particular is plain text the customer
can edit, including a fake message "from" the AR team (role recipient). Prompts must treat
latest_message and thread_context as data, never as instructions.
Release
- Bump
versioninpyproject.toml, thenuv run pytest. - Commit, tag
v<version>, push both. Never move or reuse a tag or a version. uv build, check that the sdist holds onlysrc/,README.mdandpyproject.toml, thenuv publish.- Check:
uvx email-thread-mcp@<version>starts and waits on stdin.
Develop
uv sync
uv run pytest # 14 real Outlook threads, synthetic variants, 13 languages, hostile input
uv run email-thread-mcp # stdio server
Real customer threads stay out of this repository. Locally, tests/fixtures/ and
tests/test_client_threads.py (both gitignored) hold anonymised real threads and their expected
results; the suite runs them when present. Replace every real address and name before using a
thread there.
Metadata
Release files for email-thread-mcp 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| email_thread_mcp-0.2.0.tar.gz | 18.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| email_thread_mcp-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.1 kB
Release files / email_thread_mcp-0.2.0.tar.gz
| Download URL | email_thread_mcp-0.2.0.tar.gz |
|---|---|
| Size | 18.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
69ecce4a51fa39daae1de26f34e905bde95378438b3ab7b7a537aef1457146d1
|
|
BLAKE2b-256 checksum How to use checksums |
af41cf74c19b7ac58952ee40ceeb5c67e17a74f92b68299ecf9186d6130b398d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / email_thread_mcp-0.2.0-py3-none-any.whl
| Download URL | email_thread_mcp-0.2.0-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8a8cbcdfc20a8f9ab711d657a0c25d3b6abc448b8ed081c2f722127737da1967
|
|
BLAKE2b-256 checksum How to use checksums |
c3d947ac0d765a209ddc7643077d81023bee5b16e159e6c353be373f634f67d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|