Skip to main content

email-thread-mcp

MCP server (stdio) with three 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). A table row of short cells becomes one line, cells joined by |, so an invoice stays next to its date and amount 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
assemble_output(envelope, schema?, allowed_categories?, thread?, allowed_actions?) last step of a workflow whose engine assembles the envelope from earlier steps' values with a template, so no model copies them: the validate_output checks, then a JSON Schema check. Returns the envelope plus validation contracts/assemble.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.5.1"]
    }
  }
}

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, envelope, schema, allowed_categories, allowed_actions, 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.

Use assemble_output instead when a workflow engine can build the envelope itself. A model that copies identifiers, invoice numbers or a draft into a final JSON answer changes some of them; a template that renders each value with a JSON filter does not. Its argument names (envelope, schema, allowed_categories, thread, allowed_actions) are chosen not to clash with the usual step outputs, since some engines fill an argument from a context variable of the same name. A failed check does not raise: it is reported in validation, so the caller still gets the output to review. With allowed_actions (the suggested-action vocabulary), an action that differs from a listed one only in case, punctuation or underscores is corrected; one outside the list is kept as a new action, with a warning. An argument that is still an unrendered template (a variable it uses was not set) raises an error that says so.

For the reply, two fields save the drafting model from reading the whole thread:

  • reply_to_greeting: how to address reply_to_name. If our earlier message opened "Dear Mr. Berg,", it is Mr. Berg for Jan Berg; otherwise the name as given. No honorific is added that no message states.
  • our_signature: the signature block of our quoted message, to sign the reply with. Our message is the one from ar_mailboxes or, without them, the one signed by the person the customer greets ("Dear Ms. Weiss" → Clara Weiss's message). Of that person's quoted messages, the fullest block is used, since a short follow-up is often signed with the name alone. Phone, email, URL and address lines are dropped; null when no quoted message is ours. 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, the hidden attribute) and zero-width, bidirectional and control characters, and reports both in parse_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

  1. Bump version in pyproject.toml, then uv run pytest.
  2. Commit, tag v<version>, push both. Never move or reuse a tag or a version.
  3. uv build, check that the sdist holds only src/, README.md and pyproject.toml, then uv publish.
  4. 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.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for email-thread-mcp 0.5.1
File Size Uploaded
email_thread_mcp-0.5.1.tar.gz 23.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for email-thread-mcp 0.5.1
File Interpreter ABI Platform
email_thread_mcp-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 50.5 kB

Release files / email_thread_mcp-0.5.1.tar.gz

Download URL email_thread_mcp-0.5.1.tar.gz
Size 23.4 kB
Tags Source
SHA-256 checksum
How to use checksums
28132db71656a2913aa8be0e701618ff1678739d7bad0aee6f310be2b4ba6c5d
BLAKE2b-256 checksum
How to use checksums
04a421ba994d792adf884e968672c42163b96d15bf1672afed6ff69a9c2fde3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / email_thread_mcp-0.5.1-py3-none-any.whl

Download URL email_thread_mcp-0.5.1-py3-none-any.whl
Size 27.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
536d4a3deffa368ad4538695bbe621d4dbf5e8507295ad22c8df4820dff36adc
BLAKE2b-256 checksum
How to use checksums
c644acf14b3d8de6e7856085249533db957fbeecb1c9dfce048e2e7bf850e1c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page