feat(email-mcp): add download_attachment MCP tool#67
Merged
stevenobiajulu merged 1 commit intomainfrom May 2, 2026
Merged
Conversation
…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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
download_attachmentMCP tool — agents can now retrieve attachment bytes through MCP instead of dropping down to provider-specific code. Closes #66.downloadAttachmentActioninemail-coreusing the repo-standard{ success, error }envelope (matchesmove,reply,compose-helpers).EmailAttachmentHandlerimplemented onGraphEmailProvider— bothlistAttachments(cheap metadata-only path) anddownloadAttachmentvia themicrosoft.graph.fileAttachment/contentBytesOData type cast.AttachmentNotSupportedErrorsentinel so non-fileAttachmentGraph results (itemAttachment,referenceAttachment) surface as a typedNOT_SUPPORTEDrather than a generic provider failure.max_size_mbinput (default 5, hard ceiling 25) with pre-download metadata size check and post-download buffer-length verification.wrapAction— no changes to lazy provider state or the wrapper.Scope cuts (deferred to follow-ups)
/$valueraw-bytes endpoint foritemAttachment/referenceAttachment— requires a newgetRawmethod onRealGraphApiClient.save_pathdisk-save mode — needssafeDirplumbing through MCP and path/symlink defenses; changes the tool's trust model.resource_link.Test plan
npm run build— clean across all 4 packagesnpm run lint(tsc --noEmit) — cleannpm run test— 580 tests pass (231 email-core + 161 email-mcp + 63 gmail + 125 microsoft)npm run check:spec-coverage— 186/186npm run check:server-json,check:gemini-extension-manifest— passNew tests
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), Gmailpart:*ID acceptance.listAttachmentsURL + cast,downloadAttachmenthappy path + cast, non-fileAttachmentsentinel, 404 propagation. Schema validator extended to cover single-attachment URLs and thecontentBytescast.download_attachmentname + mailbox routing + failed-initPROVIDER_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; preferredlistAttachmentsovergetMessagefor 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 towrapActionasPROVIDER_UNAVAILABLE.