hey_download_attachment
Download an email attachment by providing the email ID and attachment ID. Decodes base64 content and saves it to a specified path or default directory.
Instructions
Download a single attachment from an email and save it to disk. Decodes the base64-encoded MIME part and writes it to the supplied path (or ~/Downloads/hey-attachments/<email_id>/ by default). Use the attachment_id from hey_read_email's attachments array.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| email_id | Yes | The email ID containing the attachment | |
| attachment_id | Yes | The attachment ID from hey_read_email's attachments array (e.g. 'part-1') | |
| save_path | No | Optional absolute path or directory to save into. Defaults to ~/Downloads/hey-attachments/<email_id>/<filename>. Trailing '/' is treated as a directory. |
Implementation Reference
- src/tools/attachments.ts:410-443 (handler)The main handler function `downloadAttachment` that decodes a base64-encoded MIME part and writes it to disk. It fetches the raw RFC822 message, finds the attachment by ID, derives the filename, resolves the save path, creates directories, and writes the file.
export async function downloadAttachment(args: { emailId: string attachmentId: string savePath?: string }): Promise<{ local_path: string filename: string size: number mime: string }> { const { emailId, attachmentId } = args const { attachmentParts } = await fetchAndWalkRawMessage(emailId) const found = attachmentParts.find((a) => a.id === attachmentId) if (!found) { const ids = attachmentParts.map((a) => a.id).join(", ") || "(none)" throw new Error( `Attachment ${attachmentId} not found on email ${emailId}. Available ids: ${ids}`, ) } const filename = deriveFilename(found.part, found.index) const mime = pureMime(found.part.headers["content-type"]) const targetPath = resolveSavePath(args.savePath, emailId, filename) await mkdir(dirname(targetPath), { recursive: true }) await writeFile(targetPath, found.part.decoded) return { local_path: targetPath, filename, size: found.part.decoded.byteLength, mime, } } - src/index.ts:521-545 (registration)Tool registration in the MCP tools array: defines the 'hey_download_attachment' tool with its name, description, and inputSchema (email_id, attachment_id, save_path).
{ name: "hey_download_attachment", description: "Download a single attachment from an email and save it to disk. Decodes the base64-encoded MIME part and writes it to the supplied path (or ~/Downloads/hey-attachments/<email_id>/<filename> by default). Use the attachment_id from hey_read_email's `attachments` array.", inputSchema: { type: "object" as const, properties: { email_id: { type: "string", description: "The email ID containing the attachment", }, attachment_id: { type: "string", description: "The attachment ID from hey_read_email's attachments array (e.g. 'part-1')", }, save_path: { type: "string", description: "Optional absolute path or directory to save into. Defaults to ~/Downloads/hey-attachments/<email_id>/<filename>. Trailing '/' is treated as a directory.", }, }, required: ["email_id", "attachment_id"], }, }, - src/index.ts:1267-1310 (registration)The CallToolRequestSchema switch case for 'hey_download_attachment' which validates parameters (emailId, attachmentId, savePath) using validateId/validateAttachmentId/validateSavePath, and calls the downloadAttachment handler.
case "hey_download_attachment": { const emailId = validateId(args?.email_id) const attachmentId = validateAttachmentId(args?.attachment_id) const savePath = validateSavePath(args?.save_path) if (!emailId) { return { content: [ { type: "text", text: "Error: email_id is required and must be valid", }, ], isError: true, } } if (!attachmentId) { return { content: [ { type: "text", text: "Error: attachment_id is required (e.g. 'part-1' from hey_read_email)", }, ], isError: true, } } if (savePath === null) { return { content: [ { type: "text", text: "Error: save_path must be a non-empty string under 1024 chars", }, ], isError: true, } } result = await downloadAttachment({ emailId, attachmentId, savePath, }) break } - src/index.ts:137-143 (schema)Validation function `validateAttachmentId` that validates the attachment ID format (must match 'part-N' pattern) used by the tool.
function validateAttachmentId(value: unknown): string | null { if (typeof value !== "string") return null const trimmed = value.trim() if (trimmed.length === 0 || trimmed.length > 64) return null if (!/^part-\d+$/.test(trimmed)) return null return trimmed } - src/index.ts:151-157 (helper)Validation function `validateSavePath` that validates the optional save_path parameter for the tool.
function validateSavePath(value: unknown): string | null | undefined { if (value === undefined) return undefined if (typeof value !== "string") return null const trimmed = value.trim() if (trimmed.length === 0 || trimmed.length > 1024) return null return trimmed }