Skip to content

download_attachment rejects real Gmail attachment IDs (256-char cap) + ENAMETOOLONG on no-filename fallback #222

Description

@ShikherVerma

Summary

download_attachment is unusable for the majority of real Gmail attachments. Two independent bugs stack on the same code path. Reproduced on 1.2.0 (npm latest); the relevant code is unchanged on main, so it affects HEAD too.

Bug 1 — attachmentId inherits GmailIdSchema's 256-char cap

DownloadAttachmentSchema.attachmentId reuses GmailIdSchema, which is z.string().min(1).max(256).... That bound is right for message / thread / label / filter IDs, but Gmail attachment IDs encode the message + MIME part path and routinely run 300-600+ characters (the one in my repro was 404). The tool therefore rejects valid IDs at schema-validation time, before any API call:

MCP error -32602: Input validation error for tool download_attachment: [
  { "code": "too_big", "maximum": 256, "path": ["attachmentId"],
    "message": "Too big: expected string to have <=256 characters" }
]

Source: src/tools.ts

export const DownloadAttachmentSchema = z.object({
  messageId: GmailIdSchema.describe(...),
  attachmentId: GmailIdSchema.describe("ID of the attachment to download"), // <-- 256 cap
  ...
});

Bug 2 — no-filename fallback overflows the OS filename limit

After raising the cap, calling download_attachment without filename fails again. When the filename can't be taken from args/part, the code derives attachment-${args.attachmentId}, which is now a 400+ char single path component:

Failed to download attachment: ENAMETOOLONG: name too long, open
'/.../attachment-ANGjdJ_BeTwjxH4tExvHvt5kg-aHKfjCLRjlhBIqMdlndZCp...<~400 chars>'

Source: src/tools/downloads.ts — the attachment-${args.attachmentId} fallback (used in findAttachment and the two outer fallbacks).

Steps to reproduce

  1. npx @klodr/gmail-mcp auth --scopes=gmail.readonly
  2. search_emails { query: "has:attachment" } → pick a messageId
  3. read_email { messageId } → copy the attachment ID: (note it's > 256 chars)
  4. download_attachment { messageId, attachmentId }Bug 1 (too_big)
  5. With the cap raised but no filenameBug 2 (ENAMETOOLONG)

Proposed fix

// src/tools.ts — give attachment IDs their own schema (same base64url
// charset, higher ceiling) instead of the 256-char GmailIdSchema.
+const AttachmentIdSchema = z.string().min(1).max(2048).regex(/^[A-Za-z0-9_-]+$/);

 export const DownloadAttachmentSchema = z.object({
   messageId: GmailIdSchema.describe("ID of the email message containing the attachment"),
-  attachmentId: GmailIdSchema.describe("ID of the attachment to download"),
+  attachmentId: AttachmentIdSchema.describe("ID of the attachment to download"),
   ...
// src/tools/downloads.ts — cap the id slice in the derived-name fallback
// (3 occurrences) so a long attachmentId can't overflow the filename.
-`attachment-${args.attachmentId}`
+`attachment-${args.attachmentId.slice(0, 24)}`

Verification

Built from the v1.2.0 source with the patch above and tested end to end against a live mailbox with a read-only (gmail.readonly) token: download_attachment now saves the file correctly both with an explicit filename (writes the real name) and without one (writes a safe attachment-<short-id> name). No behavior change for any other tool (only download_attachment references attachmentId).

Happy to open a PR if useful.

Environment

  • @klodr/gmail-mcp 1.2.0 (npm latest)
  • Node 23.7.0, macOS
  • Scope: gmail.readonly

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions