Skip to content

feat: download_drive_file MCP tool — export Google Docs + download binaries (by Wren) - #433

Open
conoremclaughlin wants to merge 6 commits into
mainfrom
wren/feat/drive-download-tool
Open

feat: download_drive_file MCP tool — export Google Docs + download binaries (by Wren)#433
conoremclaughlin wants to merge 6 commits into
mainfrom
wren/feat/drive-download-tool

Conversation

@conoremclaughlin

Copy link
Copy Markdown
Owner

Summary

Drive tooling could list files and read metadata but had no way to fetch content — Myra hit this exact wall exporting the ORV side-story chapters (350+ Google Docs). New download_drive_file tool:

  • Google-native files (Docs/Sheets/Slides) → files.export. Docs default to text/plain; exportMimeType also supports text/html, text/markdown, application/pdf, application/epub+zip, .docx. Sheets → CSV.
  • Binary filesfiles.get alt=media, verbatim, 50MB cap.
  • Saves under ~/.ink/files/drive/ (same media dir Ink sessions get --add-dir access to); returns { savedPath, bytes, mimeType, exported } plus a 500-char preview for textual content — or full content when returnContent=true and under 200KB.

Designed for bulk: list a folder → download each file by ID → hand saved paths to local tools (Read, or pandoc for Doc → EPUB).

Files

  • stories/google-drive/service.ts — downloadFile() (export vs alt=media, extension mapping, size guard)
  • stories/google-drive/handlers.ts — handleDownloadDriveFile + schema; download_file added to allowlist
  • stories/google-drive/types.ts — DownloadFileOptions / DownloadedFile
  • mcp/tools/index.ts — tool registration
  • stories/google-drive/handlers.test.ts — 6 new tests

Verification

  • 16/16 Drive handler tests; full suite green (one unrelated audio-setup flake under load, passes isolated)
  • API type-check: only known baseline errors

Note

Drive OAuth scope must permit export — a read-only metadata scope returns 403; the handler surfaces the re-authorize hint.

🤖 Generated with Claude Code

— Wren

conoremclaughlin and others added 5 commits July 11, 2026 12:53
…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 conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/16
  • git diff --check origin/main...HEAD
  • git merge-tree --write-tree origin/main HEAD
  • yarn workspace @inklabs/api type-check ❌ known baseline only: channels/gateway.ts Json x4 and mcp/server.ts:451 this
  • GitHub checks at head: Unit, Integration Runtime, GitGuardian ✅; Integration DB ❌ known local Supabase permission denied for table users baseline

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

: `${baseName}${result.extension}`;
const dir = driveDownloadDir();
await mkdir(dir, { recursive: true });
const savedPath = join(dir, filename);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This final join can still resolve outside ~/.ink/files/drive in an edge case. sanitizeFilename() strips separators, but it does not reject reserved path segments like . / ..; for binary files without an extension, result.extension === "", so baseName.endsWith(result.extension) is always true and targetFilename: ".." leaves filename === "..". join(dir, "..") then resolves to the parent directory (~/.ink/files) before writeFile. A Drive file actually named .. with no extension would hit the same path. Please either reject/special-case . and .. after extension handling, and/or resolve the final path and assert it stays under driveDownloadDir() before writing. Sanitizing the appended extension too would close the same class of issue if a Drive filename ever contributes a separator-bearing extension.

— 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant