Skip to content

feat(email-mcp): add download_attachment MCP tool#67

Merged
stevenobiajulu merged 1 commit intomainfrom
fix/issue-66-download-attachment
May 2, 2026
Merged

feat(email-mcp): add download_attachment MCP tool#67
stevenobiajulu merged 1 commit intomainfrom
fix/issue-66-download-attachment

Conversation

@stevenobiajulu
Copy link
Copy Markdown
Member

Summary

Adds the download_attachment MCP tool — agents can now retrieve attachment bytes through MCP instead of dropping down to provider-specific code. Closes #66.

  • New downloadAttachmentAction in email-core using the repo-standard { success, error } envelope (matches move, reply, compose-helpers).
  • EmailAttachmentHandler implemented on GraphEmailProvider — both listAttachments (cheap metadata-only path) and downloadAttachment via the microsoft.graph.fileAttachment/contentBytes OData type cast.
  • AttachmentNotSupportedError sentinel so non-fileAttachment Graph results (itemAttachment, referenceAttachment) surface as a typed NOT_SUPPORTED rather than a generic provider failure.
  • max_size_mb input (default 5, hard ceiling 25) with pre-download metadata size check and post-download buffer-length verification.
  • Wired into MCP via the existing wrapAction — no changes to lazy provider state or the wrapper.

Scope cuts (deferred to follow-ups)

  • /$value raw-bytes endpoint for itemAttachment / referenceAttachment — requires a new getRaw method on RealGraphApiClient.
  • save_path disk-save mode — needs safeDir plumbing through MCP and path/symlink defenses; changes the tool's trust model.
  • MCP-native binary returns via resource_link.

Test plan

  • npm run build — clean across all 4 packages
  • npm run lint (tsc --noEmit) — clean
  • npm run test580 tests pass (231 email-core + 161 email-mcp + 63 gmail + 125 microsoft)
  • npm run check:spec-coverage — 186/186
  • npm run check:server-json, check:gemini-extension-manifest — pass
  • Manual end-to-end against a live mailbox once published — verify base64 round-trips to a valid file, oversize rejection works, mailbox routing works.

New tests

  • 8 action tests in attachments.test.ts: happy path, pre/post size rejection, sanitization, NOT_SUPPORTED (capability missing + sentinel error), NOT_FOUND, network-error propagation (must NOT be swallowed), Gmail part:* ID acceptance.
  • 4 Graph provider tests: listAttachments URL + cast, downloadAttachment happy path + cast, non-fileAttachment sentinel, 404 propagation. Schema validator extended to cover single-attachment URLs and the contentBytes cast.
  • 3 MCP server tests: bumped tool count 16→17, added download_attachment name + mailbox routing + failed-init PROVIDER_UNAVAILABLE.

Reviewed by

Plan was peer-reviewed by both Gemini and Codex via CLI before implementation. Key revisions from review: switched from a { ok } discriminated-union output to the established { success, error } envelope; preferred listAttachments over getMessage for metadata; tightened error-handling discipline to only emit typed errors for deterministic local conditions (capability missing, attachment id absent from metadata, size cap exceeded) and let provider/auth/network exceptions surface to wrapAction as PROVIDER_UNAVAILABLE.

…etrieval

Agents can discover attachment metadata via list_attachments, but until now
had no way to retrieve the bytes through MCP — consumers had to drop down to
provider-specific code, duplicating auth, rate-limit, and error mapping.

This change adds:

- downloadAttachmentAction in email-core with the repo-standard
  { success, error } envelope (matches move/reply/compose-helpers).
- EmailAttachmentHandler implemented on GraphEmailProvider — both
  listAttachments (cheap metadata-only path) and downloadAttachment via
  GET /me/messages/{id}/attachments/{aid} with the
  microsoft.graph.fileAttachment/contentBytes OData type cast.
- AttachmentNotSupportedError sentinel so non-fileAttachment Graph results
  (itemAttachment, referenceAttachment) surface as a typed NOT_SUPPORTED
  rather than a generic provider failure.
- max_size_mb input (default 5, hard ceiling 25) with both pre-download
  metadata size check and post-download buffer-length verification, so a
  provider that under-declares size can't blow past the cap.
- Tool registered in email-mcp via the existing wrapAction; no changes to
  the lazy provider state or wrapper needed.

Scope cuts (deferred to follow-ups):

- /$value raw-bytes endpoint for itemAttachment / referenceAttachment —
  requires a new getRaw method on RealGraphApiClient.
- save_path disk-save mode — needs safeDir plumbing through MCP and
  path/symlink defenses, and changes the tool's trust model.
- MCP-native binary returns via resource_link.

Verified: all 580 tests pass, build + tsc --noEmit clean across 4 packages,
spec coverage 186/186, server.json + gemini-extension manifests check out.

Closes #66.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/email-core/src/actions/attachments.ts 91.30% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@stevenobiajulu stevenobiajulu merged commit 2fec229 into main May 2, 2026
14 checks passed
stevenobiajulu added a commit that referenced this pull request May 2, 2026
…den download_attachment (#69)

Addresses all 5 items from peer review of #67 (issue #68):

1. MCP transport: download_attachment now returns embedded `resource` content
   (typed binary blob) alongside text metadata, instead of stuffing base64
   inside the JSON-stringified text envelope. URI scheme:
   attachment://<mailbox>/<messageId>/<attachmentId>.

2. URL encoding: added `encodeGraphPathId` helper in the Microsoft provider
   and applied it at every path-segment ID interpolation (15+ sites). Real
   Graph IDs containing `/`, `+`, `=` no longer 400/404 the request.

3. Base64 validation: Microsoft provider now strips whitespace, validates the
   base64 character set, and compares decoded length against Graph's reported
   `size`. Mismatches throw rather than silently returning truncated bytes.

4. original_filename: list_attachments and download_attachment outputs now
   carry both `filename` (sanitized) and `original_filename` (raw). Preserves
   non-ASCII names like `Räsumé.pdf` for international users.

5. Provider contract reshape: EmailAttachmentHandler.downloadAttachment now
   returns `{content, filename, mimeType, size}` instead of just `Buffer`.
   Single round-trip; fresh metadata on every call. Race-deleted attachments
   surface as ATTACHMENT_NOT_FOUND via new `AttachmentNotFoundError` sentinel
   (mapped from Graph 404 and Gmail 404).

Gmail provider also gets filename-fallback parity with collectPayloadContent
(filename → Content-ID → attachment-<path>) so CID-only inline parts don't
regress to empty names.

Tested: 610 passing (was 580). Added URL-encoding tests for downloadAttachment,
listAttachments, getMessage; malformed-base64 + size-mismatch tests; Gmail
404 mapping (message + attachment endpoints); CID and bare-attachment filename
fallbacks; non-ASCII filename round-trip; new MCP resource-content shape.

Pre-PR checks pass: build, lint, test:run, check:spec-coverage (186/186),
check:server-json, check:gemini-extension-manifest.

Reviewed by Gemini and Codex via /peer-review. Both pre-implementation
(framing scope) and post-implementation (catching the base64-whitespace bug
and the Gmail 404 / filename-fallback gaps).

Closes: #68
Ref: #67
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.

Feature: download_attachment MCP tool

1 participant