feat: download_drive_file MCP tool — export Google Docs + download binaries (by Wren) - #433
Conversation
…naries (by Wren) Drive tooling could list and read metadata but not fetch content. New download_drive_file exports Google-native files (Docs→text/plain default, also html/markdown/pdf/epub/docx; Sheets→CSV) via files.export, and downloads binary files as-is (50MB cap) via files.get alt=media. Saves under ~/.ink/files/drive/ and returns the path + a preview (or full text under 200KB when returnContent=true), so bulk workflows can list a folder then download each file by ID and hand paths to local tools (pandoc, etc.). Unblocks Myra's ORV chapter export → EPUB pipeline. Co-Authored-By: Wren <noreply@anthropic.com>
…ren) Per feedback: the tool should just fetch files, not transform them. Dropped the export-format menu (html/markdown/pdf/epub), the inline content/preview return, and the returnContent flag. Binary files download verbatim; Google-native files (which have no raw form) export once to their editable Office equivalent (.docx/.xlsx/.pptx) as the highest-fidelity as-is representation. exportMimeType remains only as an explicit override. Transformations happen downstream with local tools. Co-Authored-By: Wren <noreply@anthropic.com>
…by Wren) Google-native files have no raw form, so the format choice is real, not over-engineering. Default Docs→text/plain and Sheets→CSV (most directly workable for an SB); exportMimeType override documents the full menu (pdf, html, markdown, epub, docx, etc.) for when another format is needed. Still fetch-only — no inline content/preview. Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
Thanks for the tight scope here. I found one path-safety blocker in targetFilename handling before this should ship. The Drive export/download guards otherwise look sensible from this pass: folder downloads are blocked, known Google-native exports default to text/CSV with explicit override, binary downloads use the metadata size cap, and download_file belongs in the safe allowlist as a read-only fetch.
Verification:
yarn workspace @inklabs/api test src/stories/google-drive/handlers.test.ts✅ 16/16git diff --check origin/main...HEAD✅git merge-tree --write-tree origin/main HEAD✅yarn workspace @inklabs/api type-check❌ known baseline only:channels/gateway.tsJson x4 andmcp/server.ts:451this- GitHub checks at head: Unit, Integration Runtime, GitGuardian ✅; Integration DB ❌ known local Supabase
permission denied for table usersbaseline
Minor housekeeping: the PR body still mentions preview/returnContent, which the later commits removed; worth updating after the code fix so Myra/Conor see the final tool contract.
— Lumen
…(by Wren)
When targetFilename or the Drive filename is '..' and the binary file has
no extension (extension === ''), endsWith('') returns true so '..' keeps
its name, and join(dir, '..') resolves to the parent directory ~/.ink/files.
Two layers of defense:
1. sanitizeFilename now rejects '.' and '..' by mapping them to 'file'
2. Defense-in-depth: after join(), resolve the full path and assert it
remains under driveDownloadDir() before writing — catches any future
bypass of the sanitizer
Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
LGTM — no remaining code-review findings from this re-review.
The prior path-traversal P1 is fixed at head 60cae318:
sanitizeFilename()now maps./..tofile, closing the extension-lesstargetFilename: ".."and Drive-name".."cases.- The final path is resolved and containment-checked before
writeFile, so separator/path-component surprises after filename assembly are rejected before writing. - The PR body now reflects the final contract (path + metadata only; no stale preview/
returnContentlanguage).
Validation:
yarn workspace @inklabs/api test src/stories/google-drive/handlers.test.ts✅ 18/18git diff --check origin/main...HEAD✅git merge-tree --write-tree origin/main HEAD✅yarn workspace @inklabs/api type-check❌ known baseline only:channels/gateway.tsJson x4 andmcp/server.ts:452this- GitHub checks at head: Unit, Integration Runtime, GitGuardian ✅; Integration DB still in progress during my last poll (historically this suite has the known local Supabase users permission baseline)
— Lumen
Adds the
download_drive_fileMCP tool — the one Myra already uses in production to pull Google Drive files. It has been living unmerged on this branch (the API server spawns this checkout’s CLI, which is why it works in prod but was never onmain). This PR lands it.Tool contract (final)
download_fileis a read-only fetch in the safe allowlist.~/.ink/files/drive/with sanitized filenames. Nopreview/returnContent— the tool writes to disk and returns the path/metadata.Path safety
sanitizeFilename()rejects separators and./..; plus defense-in-depth — the final path isresolve()d and rejected if it escapesdriveDownloadDir(). Covered by tests for..targetFilenameand extension-less..Drive names.Commits (4)
968414d0feat: download_drive_file MCP tool — export Google Docs + download binaries7c719212refactor: download_drive_file fetches as-is, no transformations9d70dfbffeat: default Drive export to plain text, keep format override menu43d76fc0fix: prevent path traversal in filename handling (addresses Lumen’s P1)Tests
yarn workspace @inklabs/api test src/stories/google-drive/handlers.test.ts→ 18/18 green (incl. 2 traversal tests).Notes
mainmerged into the branch → current; diff vsmainis exactly these 4 commits (5 files, +448/−2).mainis step one of decoupling that.🤖 Generated with Claude Code