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
npx @klodr/gmail-mcp auth --scopes=gmail.readonly
search_emails { query: "has:attachment" } → pick a messageId
read_email { messageId } → copy the attachment ID: (note it's > 256 chars)
download_attachment { messageId, attachmentId } → Bug 1 (too_big)
- With the cap raised but no
filename → Bug 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
Summary
download_attachmentis unusable for the majority of real Gmail attachments. Two independent bugs stack on the same code path. Reproduced on1.2.0(npmlatest); the relevant code is unchanged onmain, so it affects HEAD too.Bug 1 —
attachmentIdinheritsGmailIdSchema's 256-char capDownloadAttachmentSchema.attachmentIdreusesGmailIdSchema, which isz.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:Source:
src/tools.tsBug 2 — no-filename fallback overflows the OS filename limit
After raising the cap, calling
download_attachmentwithoutfilenamefails again. When the filename can't be taken from args/part, the code derivesattachment-${args.attachmentId}, which is now a 400+ char single path component:Source:
src/tools/downloads.ts— theattachment-${args.attachmentId}fallback (used infindAttachmentand the two outer fallbacks).Steps to reproduce
npx @klodr/gmail-mcp auth --scopes=gmail.readonlysearch_emails { query: "has:attachment" }→ pick amessageIdread_email { messageId }→ copy the attachmentID:(note it's > 256 chars)download_attachment { messageId, attachmentId }→ Bug 1 (too_big)filename→ Bug 2 (ENAMETOOLONG)Proposed fix
Verification
Built from the
v1.2.0source with the patch above and tested end to end against a live mailbox with a read-only (gmail.readonly) token:download_attachmentnow saves the file correctly both with an explicitfilename(writes the real name) and without one (writes a safeattachment-<short-id>name). No behavior change for any other tool (onlydownload_attachmentreferencesattachmentId).Happy to open a PR if useful.
Environment
@klodr/gmail-mcp1.2.0(npmlatest)gmail.readonly