Skip to content

feat(tools): add read_email MCP tool for fetching full inbound mail bodies#8

Merged
brandwe merged 2 commits into
devfrom
feat/read-email-tool
May 27, 2026
Merged

feat(tools): add read_email MCP tool for fetching full inbound mail bodies#8
brandwe merged 2 commits into
devfrom
feat/read-email-tool

Conversation

@brandwe
Copy link
Copy Markdown
Member

@brandwe brandwe commented May 27, 2026

Summary

Adds read_email(message_id, mailbox="") to the MCP tool surface so an agent can fetch the full body and metadata of an inbound mail by its Graph message_id.

Why

The 60-second email-poll background task pushes a channel notification when mail arrives, but the body preview in that notification is capped — long forwarded mails (recipient lists, threaded replies, attached metadata) get truncated mid-content. Until now, the agent had no exposed way to fetch past the cutoff: send_email was the only mailbox-touching tool, and read_file / read_a365_text_file are scoped to OneDrive/SharePoint items rather than mailbox items.

What it does

  • Calls Graph GET /me/messages/{message_id} (or /users/{mailbox}/messages/{id} when a shared mailbox is named).
  • $select covers body (text + HTML), toRecipients, ccRecipients, bccRecipients, from, sender, subject, internetMessageHeaders, and hasAttachments.
  • Reuses the same Agent User three-hop token chain and Mail.Read scope that email_poll already uses — no new auth flow, no new scope grant required.
  • Errors mirror send_email's conventions: 401 → TokenExpiredError (auto-refresh + retry via the existing _with_token_retry wrapper); 404 / 403 / 5xx → clean {"error", "status", "message_id"} dict; bearer token never echoed in error paths.

Tests

7 new tests in tests/tools/test_read_email.py:

  • happy path: full body + all recipient lists + headers + hasAttachments returned verbatim
  • 50 KB body returned verbatim with no truncation on our side
  • mailbox parameter routes to /users/{mailbox}/messages/{id} correctly
  • 401 raises TokenExpiredError
  • 404 returns clean error dict
  • 5xx returns error dict with status
  • bearer token never leaks into error messages on failure

Full suite: 1,248 passed, 1 skipped against this branch. ruff check . clean on changed files.

Notes

  • ruff format --check . reports 45 pre-existing files on main that would be reformatted; the files this PR touches are all format-clean. Separate cleanup PR worth filing.
  • No new MCP scope grants needed — Mail.Read is already in the Agent User consent grant (scripts/create_entra_agent_ids.py).

Test plan

  • Restart any locally-running entraclaw MCP server and confirm read_email appears in the tool list.
  • Call read_email(message_id=<id-of-recent-inbox-item>) and confirm the full body, all recipient lists, and headers come back populated.
  • Optionally test the mailbox parameter against a shared mailbox the Agent User has access to.

🤖 Generated with Claude Code

evanclan and others added 2 commits May 27, 2026 14:56
Graph's receivedDateTime gt filter can re-deliver messages at the cursor's
exact second after a server restart when per-session dedup is lost. Bump the
watermark by 1 ms after each poll batch and isolate email poll tests from
blob env leakage.
…odies

The 60-second email-poll channel push truncates the body preview of
inbound mail. Long forwards (recipient lists, threaded replies,
attached metadata) get cut off mid-content, so the agent can't read
past the cut even when the message_id is right there in the push.

Adds `read_email(message_id, mailbox="")` which calls Graph
`GET /me/messages/{message_id}` (or `/users/{mailbox}/messages/{id}`
for shared mailboxes) with `$select` covering body (text + HTML), all
recipient lists, sender, subject, internetMessageHeaders, and
hasAttachments. Reuses the same Agent User token chain + `Mail.Read`
scope as `email_poll`. Errors mirror `send_email`: 401 →
TokenExpiredError (auto-refresh + retry); 404/403/5xx → clean
{"error", "status", "message_id"} dict; bearer token never echoed.

+7 tests (happy path + verbatim long body + shared mailbox + 401/404/500
+ no-token-leak).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brandwe brandwe force-pushed the feat/read-email-tool branch from 0d2d74b to 28fdfc0 Compare May 27, 2026 21:56
@brandwe brandwe changed the base branch from main to dev May 27, 2026 21:56
@brandwe brandwe merged commit 368e071 into dev May 27, 2026
5 checks passed
@brandwe brandwe deleted the feat/read-email-tool branch May 27, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants