mcp-gee-sweet 0.9.2.dev215__tar.gz → 0.9.2.dev217__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/drive_transfer.md +50 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/roadmap.md +1 -1
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/drive/transfer.py +97 -17
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/drive/test_transfer.py +372 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/notify-kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/notify-partner.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/qa-kickoff.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/roadmap-status.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/amy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/bob.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/joy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.dockerignore +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.gitignore +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/.python-version +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/CLAUDE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/Dockerfile +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/LICENSE +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/Makefile +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/assets/logo.png +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-comments-first-class.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-gmail-domain.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-local-fs-test-harness.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-pillow-image-dependency.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-prompt-qa-role.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/blockquote-representation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/borderless-table-columns.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/doc-to-markdown.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/heading-anchor-resolution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/module-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design/sse-consent-off-event-loop.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/design.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/gmail-test-plan.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/results/2026-09-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/retro-v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/runs/v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/docs_content.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/docs_layout.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/docs_style.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/docs_tables.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/drive_activity.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/drive_files.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/drive_sharing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/gmail.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/style-guide.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/scripts/write_team_mcp_configs.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/server.json +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/http_transport.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/integration/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/integration/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/integration/test_local_fs_harness.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_server_json.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/tests/test_team_mcp_configs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.9.2.
|
|
3
|
+
Version: 0.9.2.dev217
|
|
4
4
|
Summary: MCP server for Google Workspace — Sheets, Drive, Docs (HTML or Markdown), and Calendar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/khuisman/mcp-gee-sweet
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/khuisman/mcp-gee-sweet/issues
|
|
@@ -1787,6 +1787,56 @@ Unit tests: full suite 1883 passed / 3 skipped; ruff clean. Fast-path re-verific
|
|
|
1787
1787
|
|
|
1788
1788
|
---
|
|
1789
1789
|
|
|
1790
|
+
### TC-D279: downloads write to a temp file and rename it into place; a leftover temp file is never uploaded (issue #844) ⚠️ local-filesystem ⚠️ destructive
|
|
1791
|
+
|
|
1792
|
+
**Background:** `sync_folder`, `download_file`, and `download_folder` used to stream straight into the destination. A download that failed partway left a truncated file with a current mtime, so the next bidirectional sync read it as locally newer and uploaded it over the intact Drive copy. Every download now goes through `_write_atomically`: it writes a sibling `.gee-sweet-partial-<16 hex>` file, stamps `sync_folder`'s Drive mtime on it, and renames it onto the destination only when complete. A temp file survives only a hard kill mid-write, and `sync_folder` and `upload_local_folder` skip one. The failure path itself (a dropped connection mid-download) can't be triggered through the tool surface; it's unit-tested in `tests/drive/test_transfer.py::TestSyncFolderFailedDownloadLeavesNoPartial`, `TestDownloadToolsFailedDownloadLeavesNoPartial`, and `TestWriteAtomically`. This case checks the success path through the new write, the mode and mtime it leaves, and the leftover-temp-file skip.
|
|
1793
|
+
|
|
1794
|
+
**Setup**
|
|
1795
|
+
> 1. Call `create_folder(name="qa-279", parent_folder_id="{FOLDER_ID}")`; call the returned ID `{F}`.
|
|
1796
|
+
> 2. Run `rm -rf /tmp/qa-279 && mkdir -p /tmp/qa-279/src /tmp/qa-279/sync && head -c 3000000 /dev/urandom > /tmp/qa-279/src/big.bin`.
|
|
1797
|
+
> 3. Call `upload_local_file(local_path="/tmp/qa-279/src/big.bin", parent_folder_id="{F}")`; call the returned ID `{BIG}`. Call `get_file_metadata(file_id="{BIG}")` and note `modified_time` and `md5_checksum`.
|
|
1798
|
+
> 4. Plant a stale local copy and a leftover temp file: `printf 'stale' > /tmp/qa-279/sync/big.bin && chmod 640 /tmp/qa-279/sync/big.bin && touch -t 202001010000 /tmp/qa-279/sync/big.bin && printf 'half a file' > /tmp/qa-279/sync/.gee-sweet-partial-0123456789abcdef`.
|
|
1799
|
+
|
|
1800
|
+
**Steps**
|
|
1801
|
+
> 1. `sync_folder(folder_id="{F}", local_path="/tmp/qa-279/sync/")` (real run, default `bidirectional`).
|
|
1802
|
+
> 2. Run `ls -la /tmp/qa-279/sync/ && md5 -q /tmp/qa-279/sync/big.bin && stat -f '%Sp %Sm' -t '%Y-%m-%dT%H:%M:%S' /tmp/qa-279/sync/big.bin` (Linux: `md5sum`, `stat -c '%A %y'`).
|
|
1803
|
+
> 3. `list_files(folder_id="{F}")`.
|
|
1804
|
+
> 4. `sync_folder(folder_id="{F}", local_path="/tmp/qa-279/sync/")` again.
|
|
1805
|
+
> 5. `upload_local_folder(local_path="/tmp/qa-279/sync/", parent_folder_id="{F}", skip_if_exists=false)`, then `list_files(folder_id="{F}")`.
|
|
1806
|
+
> 6. `download_file(file_id="{BIG}", local_path="/tmp/qa-279/sync/big.bin")`, then `ls -la /tmp/qa-279/sync/`.
|
|
1807
|
+
> 7. `download_folder(folder_id="{F}", local_path="/tmp/qa-279/dl/", skip_if_exists=false)`, then `ls -la /tmp/qa-279/dl/`.
|
|
1808
|
+
> 8. Make a read-only local copy: `printf 'protected' > /tmp/qa-279/ro.bin && chmod 444 /tmp/qa-279/ro.bin && chmod 4644 /tmp/qa-279/dl/big.bin`. Call `download_file(file_id="{BIG}", local_path="/tmp/qa-279/ro.bin")`. Then call `download_file(file_id="{BIG}", local_path="/tmp/qa-279/dl/big.bin")` and run `ls -la /tmp/qa-279/ /tmp/qa-279/dl/ && cat /tmp/qa-279/ro.bin`. (Not as root: root can write a `0444` file.)
|
|
1809
|
+
|
|
1810
|
+
**Checks**
|
|
1811
|
+
- Step 1: `downloaded` is `["big.bin"]`. `uploaded` is empty, and `.gee-sweet-partial-0123456789abcdef` appears in no result list. `failed` is empty.
|
|
1812
|
+
- Step 2: `big.bin` is 3000000 bytes, its md5 matches setup step 3's `md5_checksum`, its mode is still `-rw-r-----` (the replaced file's mode is kept), and its mtime matches setup step 3's `modified_time` to the second (local time vs UTC aside). The only `.gee-sweet-partial-*` file in the directory is the planted `…0123456789abcdef`; no new one was left behind.
|
|
1813
|
+
- Step 3: `{F}` holds exactly one file, `big.bin`. No `.gee-sweet-partial-*` file was uploaded.
|
|
1814
|
+
- Step 4: `big.bin` in `skipped`; `uploaded` and `downloaded` empty (the restamped mtime round-trips, as in #346).
|
|
1815
|
+
- Step 5: `uploaded` lists only `big.bin` (a second copy, since `skip_if_exists=false`); the planted temp file is not uploaded and not in `failed`. `list_files` shows two `big.bin` and no `.gee-sweet-partial-*`.
|
|
1816
|
+
- Step 6: succeeds with `size_bytes` 3000000. The directory still holds only `big.bin` and the planted temp file.
|
|
1817
|
+
- Step 7: `downloaded` is `["big.bin"]` and `failed` has one duplicate-filename entry for the second `big.bin` (PR #351). `/tmp/qa-279/dl/` holds only `big.bin`, 3000000 bytes, and no `.gee-sweet-partial-*` file.
|
|
1818
|
+
- Step 8 (PR #884 QA round 1): the first `download_file` fails with a permission error. `ro.bin` still reads `protected`, still `-r--r--r--`, and no `.gee-sweet-partial-*` file is in `/tmp/qa-279/`. The second call succeeds, and `dl/big.bin` is `-rw-r--r--`: the setuid bit is not carried onto the new content.
|
|
1819
|
+
|
|
1820
|
+
**Teardown**
|
|
1821
|
+
`delete_file` on `{F}` (takes its contents with it). `rm -rf /tmp/qa-279`.
|
|
1822
|
+
|
|
1823
|
+
**Result (2026-10-04) ✅ PASS** — Sky, PR #884 round 1 (`b74ec2b`), OAuth, throwaway `qa-279` folder under `TEST_FOLDER_ID`. Drive `big.bin`: 3000000 B, md5 `f606776b…2c1a`, `modified_time` `17:42:20Z`.
|
|
1824
|
+
- **Step 1:** `downloaded: ["big.bin"]`; `uploaded`, `conflicts`, `failed` empty; the planted temp file appears in no list.
|
|
1825
|
+
- **Step 2:** `big.bin` 3000000 B, md5 matches, mode `-rw-r-----` kept, mtime `10:42:20-0700` (= `17:42:20Z`). Only the planted `…0123456789abcdef` temp file present.
|
|
1826
|
+
- **Step 3:** one file, `big.bin`.
|
|
1827
|
+
- **Step 4:** `skipped: ["big.bin"]`, nothing transferred.
|
|
1828
|
+
- **Step 5:** `uploaded: ["big.bin"]` only; `list_files` shows two `big.bin`, no temp file.
|
|
1829
|
+
- **Step 6:** `size_bytes` 3000000; directory still holds only `big.bin` + the planted temp file.
|
|
1830
|
+
- **Step 7:** `downloaded: ["big.bin"]`, one duplicate-filename `failed` entry; `dl/` holds only `big.bin`, 3000000 B.
|
|
1831
|
+
|
|
1832
|
+
The case passes, but the PR goes back for a regression this case doesn't cover: a read-only (`0444`) local destination is now silently replaced. Live, after this case: `printf protected > big.bin && chmod 444 big.bin`, then `download_file(file_id="{BIG}", local_path=".../big.bin")` returned success and left a 3000000 B `-r--r--r--` file. Before this PR the `open("wb")` raised `PermissionError` and left the file alone. See the PR comment.
|
|
1833
|
+
|
|
1834
|
+
**Result (2026-10-04) ✅ PASS** — Sky, PR #884 round 2 (`b4e7287`), step 8 only, after reconnecting to the fix. Steps 1–7 code paths are unchanged apart from the new up-front `os.access` check and the `0o777` mask. Fresh `qa-279` folder; setup plus step 7 (`download_folder` → `dl/big.bin`, 3000000 B) re-run first.
|
|
1835
|
+
- **Step 8, `ro.bin`:** `download_file` failed with `[Errno 13] Permission denied: '/tmp/qa-279/ro.bin'`. `ro.bin` still reads `protected`, still `-r--r--r--`, and there's no `.gee-sweet-partial-*` in `/tmp/qa-279/`.
|
|
1836
|
+
- **Step 8, `dl/big.bin` (`chmod 4644` first):** succeeded, `size_bytes` 3000000, and the file is now `-rw-r--r--`: setuid dropped.
|
|
1837
|
+
|
|
1838
|
+
---
|
|
1839
|
+
|
|
1790
1840
|
## `list_revisions`
|
|
1791
1841
|
|
|
1792
1842
|
### TC-D146: List revisions for a spreadsheet
|
|
@@ -315,7 +315,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
315
315
|
- [x] `reply_to_message` should honor Reply-To and handle replies to your own sent mail ([#791](https://github.com/khuisman/mcp-gee-sweet/issues/791)) (PR #815)
|
|
316
316
|
- [x] Decode message bodies using the part's charset; tolerate missing base64 padding ([#792](https://github.com/khuisman/mcp-gee-sweet/issues/792)) (PR #816)
|
|
317
317
|
- [x] `get_message`/`get_thread` return no body when Gmail delivers a large (~MB) `text/plain`/`text/html` part by `attachmentId`. Found in #803's live QA ([#825](https://github.com/khuisman/mcp-gee-sweet/issues/825)) (PR #829)
|
|
318
|
-
- [
|
|
318
|
+
- [x] `get_thread`/`get_message`: add a body-less option — a long thread trips the response-size cap and there's no other way to get its message IDs, so it can't be read at all ([#793](https://github.com/khuisman/mcp-gee-sweet/issues/793)) (PR #885)
|
|
319
319
|
- [ ] Reply-all self-exclusion (silent `getProfile` failure, send-as aliases), attachment reads blocking the event loop, attachment MIME guessing ([#802](https://github.com/khuisman/mcp-gee-sweet/issues/802))
|
|
320
320
|
- [ ] Deduplicate `send_message`/`create_draft` compose and `list_messages`/`list_threads` kwargs/pagination helpers ([#795](https://github.com/khuisman/mcp-gee-sweet/issues/795))
|
|
321
321
|
- [ ] Local-file read policy across local-path tools — in a remote/SSE deployment these read the server's own filesystem (token, SA key), and Gmail attachments can now mail them out. One consistent policy (e.g. a config switch), not per-tool patches. Decision-needed; Joy scopes ([#796](https://github.com/khuisman/mcp-gee-sweet/issues/796))
|
{mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev217}/src/mcp_gee_sweet/tools/drive/transfer.py
RENAMED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
+
import contextlib
|
|
4
|
+
import errno
|
|
3
5
|
import hashlib
|
|
4
6
|
import io
|
|
5
7
|
import logging
|
|
6
8
|
import mimetypes
|
|
7
9
|
import os
|
|
8
|
-
|
|
10
|
+
import re
|
|
11
|
+
import secrets
|
|
12
|
+
from collections.abc import Callable, Iterable
|
|
9
13
|
from dataclasses import dataclass
|
|
10
14
|
from datetime import datetime, timezone
|
|
11
15
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Literal
|
|
16
|
+
from typing import Any, BinaryIO, Literal
|
|
13
17
|
|
|
14
18
|
import markdown as _md
|
|
15
19
|
from googleapiclient.errors import HttpError
|
|
@@ -39,6 +43,15 @@ _EXPORT_MIME: dict[str, tuple[str, str]] = {
|
|
|
39
43
|
|
|
40
44
|
_SYSTEM_FILES = {".DS_Store", "Thumbs.db", "desktop.ini", ".localized"}
|
|
41
45
|
|
|
46
|
+
# Every download writes to a sibling temp file with this name and renames it
|
|
47
|
+
# onto the destination only once complete (_write_atomically, #844). A temp
|
|
48
|
+
# file only outlives its download when the process is killed mid-write;
|
|
49
|
+
# sync_folder and upload_local_folder skip one so a leftover partial is never
|
|
50
|
+
# uploaded. No part of the destination's name is embedded, so the temp name
|
|
51
|
+
# can't exceed the filesystem's name-length limit.
|
|
52
|
+
_PARTIAL_DOWNLOAD_PREFIX = ".gee-sweet-partial-"
|
|
53
|
+
_PARTIAL_DOWNLOAD_RE = re.compile(r"\.gee-sweet-partial-[0-9a-f]{16}")
|
|
54
|
+
|
|
42
55
|
_SYNC_MTIME_TOLERANCE = 5 # seconds — absorbs clock skew and upload-time drift
|
|
43
56
|
|
|
44
57
|
# use_checksum hashes every both-sides pair with a Drive md5Checksum (#716), so
|
|
@@ -375,6 +388,55 @@ def _drive_time_str(dt: datetime) -> str:
|
|
|
375
388
|
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
376
389
|
|
|
377
390
|
|
|
391
|
+
def _is_partial_download(name: str) -> bool:
|
|
392
|
+
"""Whether `name` is a temp file _write_atomically left behind."""
|
|
393
|
+
return _PARTIAL_DOWNLOAD_RE.fullmatch(name) is not None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _write_atomically(
|
|
397
|
+
dest: Path, write: Callable[[BinaryIO], object], mtime: float | None = None
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Run `write` against a sibling temp file, then rename it onto `dest`.
|
|
400
|
+
|
|
401
|
+
`dest` only ever holds its previous content or the complete new content.
|
|
402
|
+
Writing straight into `dest` left a truncated file behind when a download
|
|
403
|
+
failed partway, with a current mtime that made sync_folder treat it as the
|
|
404
|
+
newer copy and upload it over the intact Drive file (#844). `mtime`, when
|
|
405
|
+
given, is stamped on the temp file before the rename, so a failed restamp
|
|
406
|
+
also leaves `dest` untouched. The temp file is removed on any failure.
|
|
407
|
+
|
|
408
|
+
A symlink at `dest` is written through, as the old `open("wb")` did: the
|
|
409
|
+
temp file goes next to the link's target and replaces the target. The new
|
|
410
|
+
file gets the mode a fresh `open("wb")` would (0o666 less the umask), or the
|
|
411
|
+
replaced file's permission bits when there was one (never its
|
|
412
|
+
setuid/setgid/sticky bits, which an in-place write would have cleared). A
|
|
413
|
+
hard link to the old file keeps the old content, since the rename swaps in
|
|
414
|
+
a new inode.
|
|
415
|
+
|
|
416
|
+
A rename needs write permission on the directory, not the file, so an
|
|
417
|
+
existing file this process couldn't open for writing is refused up front
|
|
418
|
+
with the same PermissionError `open("wb")` raised; otherwise a file the
|
|
419
|
+
user made read-only would be silently replaced (PR #884 QA round 1).
|
|
420
|
+
"""
|
|
421
|
+
target = Path(os.path.realpath(dest)) if dest.is_symlink() else dest
|
|
422
|
+
if target.exists() and not os.access(target, os.W_OK):
|
|
423
|
+
raise PermissionError(errno.EACCES, os.strerror(errno.EACCES), str(target))
|
|
424
|
+
tmp = target.with_name(_PARTIAL_DOWNLOAD_PREFIX + secrets.token_hex(8))
|
|
425
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o666)
|
|
426
|
+
try:
|
|
427
|
+
with os.fdopen(fd, "wb") as fh:
|
|
428
|
+
write(fh)
|
|
429
|
+
with contextlib.suppress(FileNotFoundError):
|
|
430
|
+
os.chmod(tmp, target.stat().st_mode & 0o777)
|
|
431
|
+
if mtime is not None:
|
|
432
|
+
os.utime(tmp, (mtime, mtime))
|
|
433
|
+
os.replace(tmp, target)
|
|
434
|
+
except BaseException:
|
|
435
|
+
with contextlib.suppress(OSError):
|
|
436
|
+
os.unlink(tmp)
|
|
437
|
+
raise
|
|
438
|
+
|
|
439
|
+
|
|
378
440
|
def _local_mtime_str(path: Path) -> str:
|
|
379
441
|
"""_local_mtime_dt in the RFC 3339 form Drive's modifiedTime field takes."""
|
|
380
442
|
return _drive_time_str(_local_mtime_dt(path))
|
|
@@ -1328,6 +1390,8 @@ async def _sync_level(
|
|
|
1328
1390
|
continue
|
|
1329
1391
|
if skip_system_files and p.name in _SYSTEM_FILES:
|
|
1330
1392
|
continue
|
|
1393
|
+
if _is_partial_download(p.name):
|
|
1394
|
+
continue
|
|
1331
1395
|
# Only a backslash (a separator on Windows, legal in a POSIX name) can
|
|
1332
1396
|
# fail this for a name the OS listed. Such a file would upload under
|
|
1333
1397
|
# a name this check then refuses on the Drive side, so it'd never
|
|
@@ -1899,6 +1963,15 @@ async def _sync_level(
|
|
|
1899
1963
|
# The plan already routed unsafe names to 'unsafe_name'; this is
|
|
1900
1964
|
# the shared backstop, and a refusal lands as download_fail.
|
|
1901
1965
|
dest_file = _safe_local_dest(dest_dir, name, resolved_dest_dir)
|
|
1966
|
+
# Mirror what the upload branch above does in reverse: set the
|
|
1967
|
+
# local file's mtime to Drive's modifiedTime so the round trip
|
|
1968
|
+
# stays within _SYNC_MTIME_TOLERANCE on the next sync. Without
|
|
1969
|
+
# this the local mtime is "now" (write time), which is always
|
|
1970
|
+
# later than Drive's original timestamp — the next sync sees the
|
|
1971
|
+
# file as locally newer and re-uploads it, indefinitely (#346).
|
|
1972
|
+
# _write_atomically stamps it before the rename, so a failed
|
|
1973
|
+
# download never leaves a "newer" partial in place (#844).
|
|
1974
|
+
drive_ts = _drive_mtime(entry).timestamp()
|
|
1902
1975
|
if is_workspace:
|
|
1903
1976
|
target_mime = _EXPORT_MIME[export_format][0]
|
|
1904
1977
|
content = await execute_in_thread(
|
|
@@ -1907,7 +1980,9 @@ async def _sync_level(
|
|
|
1907
1980
|
)
|
|
1908
1981
|
if not isinstance(content, bytes):
|
|
1909
1982
|
content = content.encode("utf-8")
|
|
1910
|
-
await asyncio.to_thread(
|
|
1983
|
+
await asyncio.to_thread(
|
|
1984
|
+
_write_atomically, dest_file, lambda fh, c=content: fh.write(c), drive_ts
|
|
1985
|
+
)
|
|
1911
1986
|
else:
|
|
1912
1987
|
|
|
1913
1988
|
def _download_to_completion(fid=fid, dest_file=dest_file) -> None:
|
|
@@ -1915,21 +1990,16 @@ async def _sync_level(
|
|
|
1915
1990
|
fileId=fid, supportsAllDrives=True
|
|
1916
1991
|
)
|
|
1917
1992
|
request.http = thread_http(drive_service)
|
|
1918
|
-
|
|
1993
|
+
|
|
1994
|
+
def _stream(fh: BinaryIO) -> None:
|
|
1919
1995
|
downloader = MediaIoBaseDownload(fh, request)
|
|
1920
1996
|
done = False
|
|
1921
1997
|
while not done:
|
|
1922
1998
|
_, done = downloader.next_chunk()
|
|
1923
1999
|
|
|
2000
|
+
_write_atomically(dest_file, _stream, drive_ts)
|
|
2001
|
+
|
|
1924
2002
|
await asyncio.to_thread(_download_to_completion)
|
|
1925
|
-
# Mirror what the upload branch above does in reverse: set the
|
|
1926
|
-
# local file's mtime to Drive's modifiedTime so the round trip
|
|
1927
|
-
# stays within _SYNC_MTIME_TOLERANCE on the next sync. Without
|
|
1928
|
-
# this the local mtime is "now" (write time), which is always
|
|
1929
|
-
# later than Drive's original timestamp — the next sync sees the
|
|
1930
|
-
# file as locally newer and re-uploads it, indefinitely (#346).
|
|
1931
|
-
dtime = _drive_mtime(entry)
|
|
1932
|
-
os.utime(dest_file, (dtime.timestamp(), dtime.timestamp()))
|
|
1933
2003
|
size = dest_file.stat().st_size
|
|
1934
2004
|
logger.debug("Synced (download) Drive → %s%s (%d bytes)", rel_prefix, name, size)
|
|
1935
2005
|
return {"kind": "download_ok", "name": name, "bytes": size}
|
|
@@ -2628,7 +2698,9 @@ def register(tool):
|
|
|
2628
2698
|
if not folder.is_dir():
|
|
2629
2699
|
raise ValueError(f"No directory found at {local_path!r}")
|
|
2630
2700
|
|
|
2631
|
-
candidates = [
|
|
2701
|
+
candidates = [
|
|
2702
|
+
p for p in folder.iterdir() if p.is_file() and not _is_partial_download(p.name)
|
|
2703
|
+
]
|
|
2632
2704
|
if skip_system_files:
|
|
2633
2705
|
candidates = [p for p in candidates if p.name not in _SYSTEM_FILES]
|
|
2634
2706
|
|
|
@@ -2867,18 +2939,21 @@ def register(tool):
|
|
|
2867
2939
|
)
|
|
2868
2940
|
if not isinstance(content, bytes):
|
|
2869
2941
|
content = content.encode("utf-8")
|
|
2870
|
-
await asyncio.to_thread(dest
|
|
2942
|
+
await asyncio.to_thread(_write_atomically, dest, lambda fh: fh.write(content))
|
|
2871
2943
|
else:
|
|
2872
2944
|
|
|
2873
2945
|
def _download_to_completion() -> None:
|
|
2874
2946
|
request = drive_service.files().get_media(fileId=file_id, supportsAllDrives=True)
|
|
2875
2947
|
request.http = thread_http(drive_service)
|
|
2876
|
-
|
|
2948
|
+
|
|
2949
|
+
def _stream(fh: BinaryIO) -> None:
|
|
2877
2950
|
downloader = MediaIoBaseDownload(fh, request)
|
|
2878
2951
|
done = False
|
|
2879
2952
|
while not done:
|
|
2880
2953
|
_, done = downloader.next_chunk()
|
|
2881
2954
|
|
|
2955
|
+
_write_atomically(dest, _stream)
|
|
2956
|
+
|
|
2882
2957
|
await asyncio.to_thread(_download_to_completion)
|
|
2883
2958
|
|
|
2884
2959
|
size = dest.stat().st_size
|
|
@@ -3086,7 +3161,9 @@ def register(tool):
|
|
|
3086
3161
|
)
|
|
3087
3162
|
if not isinstance(content, bytes):
|
|
3088
3163
|
content = content.encode("utf-8")
|
|
3089
|
-
await asyncio.to_thread(
|
|
3164
|
+
await asyncio.to_thread(
|
|
3165
|
+
_write_atomically, candidate.dest_file, lambda fh: fh.write(content)
|
|
3166
|
+
)
|
|
3090
3167
|
else:
|
|
3091
3168
|
|
|
3092
3169
|
def _download_to_completion(
|
|
@@ -3096,12 +3173,15 @@ def register(tool):
|
|
|
3096
3173
|
fileId=fid, supportsAllDrives=True
|
|
3097
3174
|
)
|
|
3098
3175
|
request.http = thread_http(drive_service)
|
|
3099
|
-
|
|
3176
|
+
|
|
3177
|
+
def _stream(fh: BinaryIO) -> None:
|
|
3100
3178
|
downloader = MediaIoBaseDownload(fh, request)
|
|
3101
3179
|
done = False
|
|
3102
3180
|
while not done:
|
|
3103
3181
|
_, done = downloader.next_chunk()
|
|
3104
3182
|
|
|
3183
|
+
_write_atomically(dest_file, _stream)
|
|
3184
|
+
|
|
3105
3185
|
await asyncio.to_thread(_download_to_completion)
|
|
3106
3186
|
|
|
3107
3187
|
size = candidate.dest_file.stat().st_size
|