mcp-gee-sweet 0.9.2.dev238__tar.gz → 0.9.2.dev243__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.
Files changed (249) hide show
  1. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/PKG-INFO +1 -1
  2. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/roadmap.md +3 -2
  3. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/sheets/helpers.py +2 -2
  4. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/drive/test_files.py +13 -5
  5. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/drive/test_transfer.py +54 -39
  6. mcp_gee_sweet-0.9.2.dev243/tests/fakes.py +27 -0
  7. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/sheets/test_helpers.py +13 -33
  8. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/sheets/test_structure.py +15 -0
  9. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_cache.py +12 -22
  10. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_server.py +5 -0
  11. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/cleanup-worktrees.md +0 -0
  12. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/merge-pr.md +0 -0
  13. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/notify-kai.md +0 -0
  14. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/notify-partner.md +0 -0
  15. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/orchestrator.md +0 -0
  16. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/prep-for-pr.md +0 -0
  17. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/qa-kickoff.md +0 -0
  18. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/release.md +0 -0
  19. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/retro.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/roadmap-status.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/start-worktree.md +0 -0
  22. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/team-member.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/commands/verify-pr.md +0 -0
  24. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/settings.json +0 -0
  25. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/amy.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/aziz.md +0 -0
  27. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/bob.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/dev.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/joy.md +0 -0
  30. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/kai.md +0 -0
  31. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.claude/team-roles/qa.md +0 -0
  32. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.dockerignore +0 -0
  33. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  35. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/dependabot.yml +0 -0
  37. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/workflows/ci.yml +0 -0
  38. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/workflows/docs.yml +0 -0
  39. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/workflows/publish-dev.yml +0 -0
  40. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.github/workflows/release.yml +0 -0
  41. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.gitignore +0 -0
  42. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.pre-commit-config.yaml +0 -0
  43. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/.python-version +0 -0
  44. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/CHANGELOG.md +0 -0
  45. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/CLAUDE.md +0 -0
  46. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/CODE_OF_CONDUCT.md +0 -0
  47. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/CONTRIBUTING.md +0 -0
  48. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/Dockerfile +0 -0
  49. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/LICENSE +0 -0
  50. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/Makefile +0 -0
  51. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/README.md +0 -0
  52. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/SECURITY.md +0 -0
  53. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docker-compose.yml +0 -0
  54. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/assets/logo.png +0 -0
  55. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/auth.md +0 -0
  56. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/client-setup.md +0 -0
  57. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/configuration.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-async-tool-execution.md +0 -0
  59. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  60. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-cache-invalidation.md +0 -0
  61. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-chart-theming.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-comments-first-class.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-composite-tools.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-dev-team.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-docs-formatting.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-fork.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-gmail-domain.md +0 -0
  70. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  71. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-prompt-qa-role.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-publishing.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-release-docs-roles.md +0 -0
  77. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-repositioning.md +0 -0
  78. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  79. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  80. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-testing.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/decision-tool-error-visibility.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/decisions/index.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/blockquote-representation.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/borderless-table-columns.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/doc-to-markdown.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/docs-ast-pipeline.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/effectiveformat-spike.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/heading-anchor-resolution.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/image-conversion.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/index.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/markdown-support.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/module-history.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/phase3-theme-system.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design/sse-consent-off-event-loop.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/design.md +0 -0
  99. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/index.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/known-limitations.md +0 -0
  101. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/notes-read.md +0 -0
  102. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/README.md +0 -0
  103. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  104. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  105. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  106. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  107. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  108. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  109. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  110. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  111. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  112. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  113. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  114. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  115. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  116. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  117. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  118. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  119. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  120. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  121. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/fixtures.template.md +0 -0
  122. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/gmail-test-plan.md +0 -0
  123. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/operations.yaml +0 -0
  124. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/playwright_oauth.md +0 -0
  125. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/.gitkeep +0 -0
  126. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-02.md +0 -0
  127. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-14.md +0 -0
  128. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-17.md +0 -0
  129. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-18.md +0 -0
  130. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  131. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-06-28.md +0 -0
  133. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-07-04-docs.md +0 -0
  134. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  135. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-07-04-infra.md +0 -0
  136. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-07-04.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/results/2026-09-04.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/retro-v0.8.0.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/retro-v0.9.0.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/run.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/runs/README.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/runs/v0.8.0.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/runs/v0.8.1.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/runs/v0.9.0.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/setup.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/calendar.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/docs_content.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/docs_layout.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/docs_style.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/docs_tables.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/drive_activity.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/drive_files.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/drive_sharing.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/drive_transfer.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/gmail.md +0 -0
  157. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/infra.md +0 -0
  158. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/sheets_charts.md +0 -0
  159. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/sheets_mgmt.md +0 -0
  160. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/sheets_read.md +0 -0
  161. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/qa/tests/sheets_write.md +0 -0
  162. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/style-guide.md +0 -0
  163. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/docs/tools.md +0 -0
  164. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/mkdocs.yml +0 -0
  165. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/pyproject.toml +0 -0
  166. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/pyrightconfig.json +0 -0
  167. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/check_supports_all_drives.py +0 -0
  168. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/gen_tool_docs.py +0 -0
  169. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/lane_context_hook.py +0 -0
  170. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/oauth_setup.py +0 -0
  171. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/qa_cancel_insert_local_images.py +0 -0
  172. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/qa_gmail_fixtures.py +0 -0
  173. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/setup_team.sh +0 -0
  174. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/scripts/write_team_mcp_configs.sh +0 -0
  175. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/server.json +0 -0
  176. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/.env.template +0 -0
  177. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/__init__.py +0 -0
  178. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/auth.py +0 -0
  179. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/cache.py +0 -0
  180. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/http_transport.py +0 -0
  181. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/server.py +0 -0
  182. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/cache.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  190. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
  191. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  193. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  194. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  197. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  198. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  199. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/pagination.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/__init__.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/conftest.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/drive/__init__.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/drive/test_activity.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/drive/test_sharing.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/integration/__init__.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/integration/conftest.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/integration/test_local_fs_harness.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/sheets/__init__.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/sheets/test_data.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_auth.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_calendar.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_check_supports_all_drives.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_concurrency.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_anchors.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_comments.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_content.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_core.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_editing.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_images.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_indices.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_layout.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_markdown_export.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_named_ranges.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_style.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_docs_tables.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_gen_tool_docs.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_gmail.py +0 -0
  243. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_lane_context_hook.py +0 -0
  244. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_qa_gmail_fixtures.py +0 -0
  245. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_response_limits.py +0 -0
  246. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_server_json.py +0 -0
  247. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_sse_shutdown.py +0 -0
  248. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/tests/test_team_mcp_configs.py +0 -0
  249. {mcp_gee_sweet-0.9.2.dev238 → mcp_gee_sweet-0.9.2.dev243}/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.dev238
3
+ Version: 0.9.2.dev243
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
@@ -193,7 +193,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
193
193
  - [x] Lock in the isolated-run rendering rule — an isolated bullet run renders at its true depth (`depth N` → `nestingLevel N`) at *every* depth, never clamped (decided 2026-09-09: consistency over avoiding orphaned indent; the markup generator owns the depth it emits). Extend TC-DOC190 with a `min_depth >= 2` case; state the rule in `emitter.py`'s bullet-pass comment and the `docs/emitter.py` paragraph of `CLAUDE.md` (PR #731) ([#714](https://github.com/khuisman/mcp-gee-sweet/issues/714))
194
194
  - [x] `create_doc` markdown: a fenced code block whose body is raw HTML tags renders as loose plain paragraphs (fence ignored, indentation flattened) instead of a monospaced code block — python-markdown's raw-HTML-block handling appears to pre-empt `fenced_code`. Surfaced during #714 triage (PR #730) ([#719](https://github.com/khuisman/mcp-gee-sweet/issues/719))
195
195
  - [ ] **Bundle:** Docs write side (emitter, html_parser, images, content), one PR closing [#509](https://github.com/khuisman/mcp-gee-sweet/issues/509), [#567](https://github.com/khuisman/mcp-gee-sweet/issues/567), [#581](https://github.com/khuisman/mcp-gee-sweet/issues/581), [#638](https://github.com/khuisman/mcp-gee-sweet/issues/638), [#715](https://github.com/khuisman/mcp-gee-sweet/issues/715) — lane A ([#894](https://github.com/khuisman/mcp-gee-sweet/issues/894))
196
- - [ ] **Bundle:** Calendar hardening, one PR closing [#459](https://github.com/khuisman/mcp-gee-sweet/issues/459), [#615](https://github.com/khuisman/mcp-gee-sweet/issues/615), [#626](https://github.com/khuisman/mcp-gee-sweet/issues/626), [#627](https://github.com/khuisman/mcp-gee-sweet/issues/627), [#726](https://github.com/khuisman/mcp-gee-sweet/issues/726) — lane B ([#890](https://github.com/khuisman/mcp-gee-sweet/issues/890))
196
+ - [x] **Bundle:** Calendar hardening, one PR closing [#459](https://github.com/khuisman/mcp-gee-sweet/issues/459), [#615](https://github.com/khuisman/mcp-gee-sweet/issues/615), [#626](https://github.com/khuisman/mcp-gee-sweet/issues/626), [#627](https://github.com/khuisman/mcp-gee-sweet/issues/627), [#726](https://github.com/khuisman/mcp-gee-sweet/issues/726) — lane B (PR #912) ([#890](https://github.com/khuisman/mcp-gee-sweet/issues/890))
197
197
  - [x] `delete_file(permanent=True)` surfaces a misleading 404 when the caller can trash but not permanently delete (Shared Drive Content manager) — an agent can read it as "already gone" and skip cleanup; check `capabilities.canDelete` first — lane B ([#876](https://github.com/khuisman/mcp-gee-sweet/issues/876)) (PR #888)
198
198
  - [x] SSE: SIGTERM with an SSE stream open logs an "Exception in ASGI application" traceback; the server still exits cleanly, pre-existing before #833 — lane A (PR #911) ([#868](https://github.com/khuisman/mcp-gee-sweet/issues/868))
199
199
  - [x] `sync_folder`'s `convert_markdown` restamp-failure handling: quota-message gap, duplicated create()+update() try/except, test fixture isolation — companion to #420 (PR #669) ([#650](https://github.com/khuisman/mcp-gee-sweet/issues/650))
@@ -214,6 +214,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
214
214
  - [x] auth: under `--transport sse`, the OAuth consent wait runs synchronously on the event loop, stalling every other session and ignoring SIGTERM until consent or timeout. #811 follow-up, found in PR #828 ([#833](https://github.com/khuisman/mcp-gee-sweet/issues/833)) (PR #867)
215
215
  - [x] Tool errors are opaque on PyPI installs: `mcp` 2.3 (what `uvx` resolves; the lock pins 2.0.0) hides any non-`ToolError` exception text, so #811's "run `mcp-gee-sweet auth`" message and uncaught Google API errors reach the client as a bare `Error executing tool <name>`. Observed live. Urgent: lane-a, next after #844 ([#872](https://github.com/khuisman/mcp-gee-sweet/issues/872)) (PR #905)
216
216
  - [ ] auth: offer OAuth re-authorization from the failing tool call, via URL-mode elicitation with the consent URL in a `ToolError` as fallback, then rebuild services in place. Covers a refresh token revoked mid-session too. No unprompted browser. Lane-a, after #872 ([#873](https://github.com/khuisman/mcp-gee-sweet/issues/873))
217
+ - [ ] sheets: an A1 range with a dangling colon (`A1:`, `A:`, `5:`, `:5`, `:B`, `:B5`) passes `_parse_a1_notation` and widens through `_grid_range` to the whole sheet or a bigger rectangle. Hits every `_grid_range_or_error` tool; for `sort_range`/`merge_cells` it changes data. Found reviewing community PR #922, which rejects only a bare `":"`. Start after #922 merges (same lines) ([#926](https://github.com/khuisman/mcp-gee-sweet/issues/926))
217
218
 
218
219
  **Sheets hardening** _(triaged out of `backlog` 2026-09-11 — see the "Backlog triage" note below)_
219
220
  - [x] `update_sheet_properties`/dimension-tool hardening + dedup follow-ups from PR #321's review ([#323](https://github.com/khuisman/mcp-gee-sweet/issues/323)) (PR #734)
@@ -272,7 +273,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
272
273
  **Calendar hardening** _(triaged out of `backlog` 2026-09-11)_
273
274
 
274
275
  **Testing** _(triaged out of `backlog` 2026-09-11)_
275
- - [ ] **Bundle:** test-suite tightening, one PR closing [#392](https://github.com/khuisman/mcp-gee-sweet/issues/392), [#526](https://github.com/khuisman/mcp-gee-sweet/issues/526), [#551](https://github.com/khuisman/mcp-gee-sweet/issues/551), [#561](https://github.com/khuisman/mcp-gee-sweet/issues/561), [#574](https://github.com/khuisman/mcp-gee-sweet/issues/574) — lane B ([#891](https://github.com/khuisman/mcp-gee-sweet/issues/891))
276
+ - [x] **Bundle:** test-suite tightening, one PR closing [#392](https://github.com/khuisman/mcp-gee-sweet/issues/392), [#526](https://github.com/khuisman/mcp-gee-sweet/issues/526), [#551](https://github.com/khuisman/mcp-gee-sweet/issues/551), [#561](https://github.com/khuisman/mcp-gee-sweet/issues/561), [#574](https://github.com/khuisman/mcp-gee-sweet/issues/574) — lane B (PR #924) ([#891](https://github.com/khuisman/mcp-gee-sweet/issues/891))
276
277
  - [ ] `qa`: `search_files` has no unit test coverage for the error-dict path ([#573](https://github.com/khuisman/mcp-gee-sweet/issues/573))
277
278
 
278
279
  **Infrastructure**
@@ -65,9 +65,9 @@ def _parse_a1_notation(range_str: str) -> dict[str, int]:
65
65
  if not range_str:
66
66
  raise ValueError("Invalid A1 notation: empty string")
67
67
 
68
- match = re.match(r"^([A-Z]+)?(\d+)?(?::([A-Z]+)?(\d+)?)?$", range_str.upper())
68
+ match = re.fullmatch(r"([A-Z]+)?(\d+)?(?::([A-Z]+)?(\d+)?)?", range_str.upper())
69
69
 
70
- if not match:
70
+ if not match or not any(match.groups()):
71
71
  raise ValueError(f"Invalid A1 notation: {range_str}")
72
72
 
73
73
  start_col, start_row, end_col, end_row = match.groups()
@@ -34,6 +34,14 @@ _drive_tool, _drive_tools = _make_tool_registry()
34
34
  drive_files_module.register(_drive_tool)
35
35
 
36
36
 
37
+ def _assert_error_list(result, underlying: str) -> None:
38
+ """The "error dict, not raised" contract: one {"error": ...} item carrying the
39
+ underlying exception's text. Doesn't pin the message's prefix wording (#574)."""
40
+ assert len(result) == 1
41
+ assert set(result[0]) == {"error"}
42
+ assert underlying in result[0]["error"]
43
+
44
+
37
45
  class TestSearchSpreadsheets:
38
46
  """Bug: single quotes in query were interpolated raw into the Drive query string."""
39
47
 
@@ -77,7 +85,7 @@ class TestSearchSpreadsheets:
77
85
  )
78
86
  ctx = _make_ctx(drive_service=drive_svc)
79
87
  result = await _drive_tools["search_spreadsheets"](query="budget 2024", ctx=ctx)
80
- assert result == [{"error": "Search failed: simulated API failure"}]
88
+ _assert_error_list(result, "simulated API failure")
81
89
 
82
90
  async def test_requests_starred_field(self):
83
91
  """#388: search_spreadsheets shares search_files' starred exposure."""
@@ -184,7 +192,7 @@ class TestSearchFiles:
184
192
  )
185
193
  ctx = _make_ctx(drive_service=svc)
186
194
  result = await _drive_tools["search_files"](query="budget", ctx=ctx)
187
- assert result == [{"error": "Search failed: simulated API failure"}]
195
+ _assert_error_list(result, "simulated API failure")
188
196
 
189
197
 
190
198
  class TestFileMutations:
@@ -721,7 +729,7 @@ class TestListSharedWithMe:
721
729
  )
722
730
  ctx = _make_ctx(drive_service=svc)
723
731
  result = await _drive_tools["list_shared_with_me"](ctx=ctx)
724
- assert result == [{"error": "List shared with me failed: simulated API failure"}]
732
+ _assert_error_list(result, "simulated API failure")
725
733
 
726
734
  async def test_max_results_capped_at_200(self):
727
735
  svc = self._drive_service()
@@ -806,7 +814,7 @@ class TestListRecentFiles:
806
814
  )
807
815
  ctx = _make_ctx(drive_service=svc)
808
816
  result = await _drive_tools["list_recent_files"](ctx=ctx)
809
- assert result == [{"error": "List recent files failed: simulated API failure"}]
817
+ _assert_error_list(result, "simulated API failure")
810
818
 
811
819
 
812
820
  class TestListFiles:
@@ -920,7 +928,7 @@ class TestListFiles:
920
928
  )
921
929
  ctx = self._ctx(svc)
922
930
  result = await _drive_tools["list_files"](folder_id="folder1", ctx=ctx)
923
- assert result == [{"error": "List files failed: simulated API failure"}]
931
+ _assert_error_list(result, "simulated API failure")
924
932
 
925
933
  async def test_requests_next_page_token(self):
926
934
  """#698: nextPageToken's absence is what marks a listing exhaustive."""
@@ -5115,11 +5115,22 @@ class TestDownloadFolder:
5115
5115
  assert result["downloaded"] == ["Doc One.pdf"]
5116
5116
  assert result["failed"] == []
5117
5117
 
5118
- async def test_skip_if_exists_true_skips_existing_and_does_not_redownload(self, tmp_path):
5118
+ @pytest.mark.parametrize(
5119
+ ("skip_if_exists", "skipped", "downloaded", "export_calls", "final_bytes"),
5120
+ [
5121
+ (True, ["Doc One.pdf"], [], 0, b"already here"),
5122
+ (False, [], ["Doc One.pdf"], 1, b"new content"),
5123
+ ],
5124
+ ids=["skip", "redownload"],
5125
+ )
5126
+ async def test_skip_if_exists_with_existing_destination(
5127
+ self, tmp_path, skip_if_exists, skipped, downloaded, export_calls, final_bytes
5128
+ ):
5119
5129
  """TC-D109 (v0.8.0 QA run, #227): skip_if_exists was live-SKIPped as
5120
- local-filesystem-dependent and had no unit coverage. A destination file
5121
- that already exists must be left untouched — the candidate never reaches
5122
- export()/get_media() at all."""
5130
+ local-filesystem-dependent and had no unit coverage. With the flag on, an
5131
+ existing destination file is left untouched and the candidate never
5132
+ reaches export(); with it off, the file is overwritten by a fresh export.
5133
+ One parametrized test so the shared mock can't drift between cases (#561)."""
5123
5134
  svc = MagicMock()
5124
5135
  svc.files.return_value.list.return_value.execute.return_value = {
5125
5136
  "files": [
@@ -5130,6 +5141,7 @@ class TestDownloadFolder:
5130
5141
  },
5131
5142
  ]
5132
5143
  }
5144
+ svc.files.return_value.export.return_value.execute.return_value = b"new content"
5133
5145
  dest = tmp_path / "Doc One.pdf"
5134
5146
  dest.write_bytes(b"already here")
5135
5147
 
@@ -5137,44 +5149,14 @@ class TestDownloadFolder:
5137
5149
  folder_id="root",
5138
5150
  local_path=str(tmp_path),
5139
5151
  export_format="pdf",
5140
- skip_if_exists=True,
5152
+ skip_if_exists=skip_if_exists,
5141
5153
  ctx=self._ctx(svc),
5142
5154
  )
5143
- assert result["skipped"] == ["Doc One.pdf"]
5144
- assert result["downloaded"] == []
5155
+ assert result["skipped"] == skipped
5156
+ assert result["downloaded"] == downloaded
5145
5157
  assert result["failed"] == []
5146
- svc.files.return_value.export.assert_not_called()
5147
- assert dest.read_bytes() == b"already here"
5148
-
5149
- async def test_skip_if_exists_false_redownloads_existing_file(self, tmp_path):
5150
- """Companion to the skip_if_exists=True case above: with the flag off, an
5151
- existing destination file is not treated as a skip candidate and gets
5152
- overwritten by a fresh export."""
5153
- svc = MagicMock()
5154
- svc.files.return_value.list.return_value.execute.return_value = {
5155
- "files": [
5156
- {
5157
- "id": "doc1",
5158
- "name": "Doc One",
5159
- "mimeType": "application/vnd.google-apps.document",
5160
- },
5161
- ]
5162
- }
5163
- svc.files.return_value.export.return_value.execute.return_value = b"new content"
5164
- dest = tmp_path / "Doc One.pdf"
5165
- dest.write_bytes(b"stale content")
5166
-
5167
- result = await _transfer_tools["download_folder"](
5168
- folder_id="root",
5169
- local_path=str(tmp_path),
5170
- export_format="pdf",
5171
- skip_if_exists=False,
5172
- ctx=self._ctx(svc),
5173
- )
5174
- assert result["downloaded"] == ["Doc One.pdf"]
5175
- assert result["skipped"] == []
5176
- svc.files.return_value.export.assert_called_once()
5177
- assert dest.read_bytes() == b"new content"
5158
+ assert svc.files.return_value.export.call_count == export_calls
5159
+ assert dest.read_bytes() == final_bytes
5178
5160
 
5179
5161
  async def test_mime_type_filter_appended_to_drive_query(self, tmp_path):
5180
5162
  """TC-D110 (v0.8.0 QA run, #227): mime_type_filter is delegated entirely to
@@ -5227,6 +5209,13 @@ class TestDownloadFile:
5227
5209
  def _metadata(self, name, mime_type):
5228
5210
  return {"name": name, "mimeType": mime_type}
5229
5211
 
5212
+ def _assert_metadata_call(self, svc, file_id):
5213
+ """#551: supportsAllDrives=True is what lets a shared-drive file
5214
+ resolve, and `fields` must include both keys download_file reads."""
5215
+ svc.files.return_value.get.assert_called_once_with(
5216
+ fileId=file_id, fields="name, mimeType", supportsAllDrives=True
5217
+ )
5218
+
5230
5219
  async def test_google_doc_export(self, tmp_path):
5231
5220
  svc = MagicMock()
5232
5221
  svc.files.return_value.get.return_value.execute.return_value = self._metadata(
@@ -5244,6 +5233,7 @@ class TestDownloadFile:
5244
5233
  svc.files.return_value.export.assert_called_once_with(
5245
5234
  fileId="doc1", mimeType="application/pdf"
5246
5235
  )
5236
+ self._assert_metadata_call(svc, "doc1")
5247
5237
  dest = tmp_path / "My Doc.pdf"
5248
5238
  assert result == {
5249
5239
  "local_path": str(dest),
@@ -5274,6 +5264,30 @@ class TestDownloadFile:
5274
5264
  assert result["local_path"] == str(dest)
5275
5265
  assert dest.read_bytes() == b"xlsx bytes"
5276
5266
 
5267
+ async def test_str_export_content_is_utf8_encoded(self, tmp_path):
5268
+ """#551: googleapiclient can hand back a text export (csv/html/txt) as
5269
+ str rather than bytes; download_file encodes it as UTF-8 before writing."""
5270
+ svc = MagicMock()
5271
+ svc.files.return_value.get.return_value.execute.return_value = self._metadata(
5272
+ "My Sheet", "application/vnd.google-apps.spreadsheet"
5273
+ )
5274
+ text = "name,city\nZoë,Zürich\n"
5275
+ svc.files.return_value.export.return_value.execute.return_value = text
5276
+
5277
+ result = await _transfer_tools["download_file"](
5278
+ file_id="sheet1",
5279
+ local_path=str(tmp_path),
5280
+ export_format="csv",
5281
+ ctx=self._ctx(svc),
5282
+ )
5283
+
5284
+ svc.files.return_value.export.assert_called_once_with(fileId="sheet1", mimeType="text/csv")
5285
+ self._assert_metadata_call(svc, "sheet1")
5286
+ dest = tmp_path / "My Sheet.csv"
5287
+ expected = text.encode("utf-8")
5288
+ assert dest.read_bytes() == expected
5289
+ assert result["size_bytes"] == len(expected)
5290
+
5277
5291
  async def test_explicit_local_path_overrides_drive_filename(self, tmp_path):
5278
5292
  """When local_path names an exact file (not a directory), that name is
5279
5293
  used as-is rather than Drive's own name + a guessed extension — the
@@ -5330,6 +5344,7 @@ class TestDownloadFile:
5330
5344
  svc.files.return_value.get_media.assert_called_once_with(
5331
5345
  fileId="bin1", supportsAllDrives=True
5332
5346
  )
5347
+ self._assert_metadata_call(svc, "bin1")
5333
5348
  dest = tmp_path / "photo.png"
5334
5349
  assert result == {"local_path": str(dest), "name": "photo.png", "size_bytes": len(content)}
5335
5350
  assert dest.read_bytes() == content
@@ -0,0 +1,27 @@
1
+ """Shared fake Google API services for unit tests."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+
6
+ class FakeSheetsService:
7
+ """A minimal spreadsheets().get(...).execute() stand-in. Give it either
8
+ `result` (returned by every execute() call) or `exception` (raised
9
+ instead). Shared so a change to the mocked call shape is made once
10
+ (#392)."""
11
+
12
+ _http = SimpleNamespace(credentials=None)
13
+
14
+ def __init__(self, *, result: dict | None = None, exception: Exception | None = None):
15
+ self._result = result
16
+ self._exception = exception
17
+
18
+ def spreadsheets(self):
19
+ return self
20
+
21
+ def get(self, spreadsheetId, fields):
22
+ return self
23
+
24
+ def execute(self, **kwargs):
25
+ if self._exception is not None:
26
+ raise self._exception
27
+ return self._result
@@ -1,5 +1,3 @@
1
- from types import SimpleNamespace
2
-
3
1
  import pytest
4
2
 
5
3
  from mcp_gee_sweet.cache import SheetInfo, SheetStructureCache
@@ -11,6 +9,8 @@ from mcp_gee_sweet.tools.sheets.helpers import (
11
9
  _parse_a1_notation,
12
10
  )
13
11
 
12
+ from ..fakes import FakeSheetsService
13
+
14
14
 
15
15
  class TestColumnIndexToLetter:
16
16
  def test_single_letters(self):
@@ -48,6 +48,11 @@ class TestLetterToColumnIndex:
48
48
 
49
49
 
50
50
  class TestParseA1Notation:
51
+ @pytest.mark.parametrize("range_str", [":", "A1\n", "1:2\n", "B:D\n"])
52
+ def test_incomplete_or_trailing_newline_range_raises(self, range_str):
53
+ with pytest.raises(ValueError, match="Invalid A1 notation"):
54
+ _parse_a1_notation(range_str)
55
+
51
56
  def test_single_cell(self):
52
57
  result = _parse_a1_notation("A1")
53
58
  assert result["startColumnIndex"] == 0
@@ -133,31 +138,6 @@ class TestParseA1Notation:
133
138
  _parse_a1_notation("B1:A1")
134
139
 
135
140
 
136
- class _FakeSheetsService:
137
- """A minimal spreadsheets().get(...).execute() stand-in. Give it either
138
- `result` (returned by every execute() call) or `exception` (raised
139
- instead) — covers the transient-failure, genuine-not-found, and
140
- real-match fixture shapes that used to be three separate, near-identical
141
- hand-rolled classes (PR #754 review)."""
142
-
143
- _http = SimpleNamespace(credentials=None)
144
-
145
- def __init__(self, *, result: dict | None = None, exception: Exception | None = None):
146
- self._result = result
147
- self._exception = exception
148
-
149
- def spreadsheets(self):
150
- return self
151
-
152
- def get(self, spreadsheetId, fields):
153
- return self
154
-
155
- def execute(self, **kwargs):
156
- if self._exception is not None:
157
- raise self._exception
158
- return self._result
159
-
160
-
161
141
  _ONE_OTHER_SHEET = {"sheets": [{"properties": {"title": "Other", "sheetId": 0}}]}
162
142
  _TWO_SHEETS = {
163
143
  "sheets": [
@@ -167,21 +147,21 @@ _TWO_SHEETS = {
167
147
  }
168
148
 
169
149
 
170
- def _raising_service() -> _FakeSheetsService:
150
+ def _raising_service() -> FakeSheetsService:
171
151
  """Simulates a transient API failure (rate limit, timeout, auth hiccup)."""
172
- return _FakeSheetsService(exception=TimeoutError("simulated transient API failure"))
152
+ return FakeSheetsService(exception=TimeoutError("simulated transient API failure"))
173
153
 
174
154
 
175
- def _empty_service() -> _FakeSheetsService:
155
+ def _empty_service() -> FakeSheetsService:
176
156
  """A real API response where the sheet genuinely doesn't exist."""
177
- return _FakeSheetsService(result=_ONE_OTHER_SHEET)
157
+ return FakeSheetsService(result=_ONE_OTHER_SHEET)
178
158
 
179
159
 
180
- def _matching_service() -> _FakeSheetsService:
160
+ def _matching_service() -> FakeSheetsService:
181
161
  """A real API response with more than one sheet, so a happy-path lookup
182
162
  exercises picking the right match rather than trivially returning the
183
163
  only entry present."""
184
- return _FakeSheetsService(result=_TWO_SHEETS)
164
+ return FakeSheetsService(result=_TWO_SHEETS)
185
165
 
186
166
 
187
167
  class TestGetSheetIdExceptionPropagation:
@@ -3,6 +3,8 @@
3
3
  from itertools import groupby
4
4
  from unittest.mock import AsyncMock, MagicMock
5
5
 
6
+ import pytest
7
+
6
8
  from mcp_gee_sweet.cache import SheetInfo
7
9
  from mcp_gee_sweet.tools.sheets import structure as sheets_structure_module
8
10
 
@@ -1260,6 +1262,19 @@ class TestResizeColumns:
1260
1262
 
1261
1263
 
1262
1264
  class TestFormatCells:
1265
+ @pytest.mark.parametrize("range_str", [":", "A1\n"])
1266
+ async def test_malformed_range_does_not_format_any_cells(self, range_str):
1267
+ svc = self._sheets_service()
1268
+ result = await _structure_tools["format_cells"](
1269
+ spreadsheet_id="ss1",
1270
+ sheet="Sheet1",
1271
+ range=range_str,
1272
+ bold=True,
1273
+ ctx=_make_ctx(sheets_service=svc, cache=None),
1274
+ )
1275
+ assert "error" in result
1276
+ svc.spreadsheets.return_value.batchUpdate.assert_not_called()
1277
+
1263
1278
  def _sheets_service(self, sheet_id=0):
1264
1279
  mock = MagicMock()
1265
1280
  mock.spreadsheets.return_value.get.return_value.execute.return_value = {
@@ -19,6 +19,8 @@ from mcp_gee_sweet.cache import (
19
19
  )
20
20
  from mcp_gee_sweet.tools.sheets.helpers import _get_sheet_id
21
21
 
22
+ from .fakes import FakeSheetsService
23
+
22
24
  # All tests use an in-memory SQLite database.
23
25
  DB = ":memory:"
24
26
 
@@ -458,24 +460,10 @@ class TestCalendarCacheSetTtl:
458
460
  assert cache.get_ttl() == 5
459
461
 
460
462
 
461
- class _FakeSheetsService:
462
- """Returns a spreadsheet with a single sheet named 'New', id 1."""
463
-
464
- _http = SimpleNamespace(credentials=None)
465
-
466
- class _Spreadsheets:
467
- class _Request:
468
- def execute(self, **kwargs):
469
- return {
470
- "properties": {"title": "New Title"},
471
- "sheets": [{"properties": {"title": "New", "sheetId": 1}}],
472
- }
473
-
474
- def get(self, spreadsheetId, fields):
475
- return self._Request()
476
-
477
- def spreadsheets(self):
478
- return self._Spreadsheets()
463
+ _NEW_SHEET = {
464
+ "properties": {"title": "New Title"},
465
+ "sheets": [{"properties": {"title": "New", "sheetId": 1}}],
466
+ }
479
467
 
480
468
 
481
469
  class _FakeDriveService:
@@ -500,7 +488,9 @@ class TestFetchSheetsModifiedTimeValidation:
500
488
  cache = SheetStructureCache(db_path=DB, ttl=1000)
501
489
  cache.store("sid", [SheetInfo(title="Old", sheet_id=0)], modified_time="v1")
502
490
 
503
- result = await fetch_sheets(_FakeSheetsService(), "sid", cache, _FakeDriveService())
491
+ result = await fetch_sheets(
492
+ FakeSheetsService(result=_NEW_SHEET), "sid", cache, _FakeDriveService()
493
+ )
504
494
 
505
495
  assert len(result) == 1
506
496
  assert result[0].title == "New"
@@ -510,8 +500,8 @@ class TestFetchSheetsModifiedTimeValidation:
510
500
  cache.store("sid", [SheetInfo(title="Old", sheet_id=0)], modified_time="v1")
511
501
 
512
502
  # No drive_service passed → no modifiedTime check → TTL-valid cache hit,
513
- # even though _FakeSheetsService would return a different sheet.
514
- result = await fetch_sheets(_FakeSheetsService(), "sid", cache)
503
+ # even though the Sheets fake would return a different sheet.
504
+ result = await fetch_sheets(FakeSheetsService(result=_NEW_SHEET), "sid", cache)
515
505
 
516
506
  assert result[0].title == "Old"
517
507
 
@@ -533,7 +523,7 @@ class TestGetSheetIdModifiedTimePropagation:
533
523
  cache.mark_dirty("sid") # force the next lookup to be a genuine miss
534
524
 
535
525
  sheet_id = await _get_sheet_id(
536
- _FakeSheetsService(), "sid", "New", cache, _FakeDriveService()
526
+ FakeSheetsService(result=_NEW_SHEET), "sid", "New", cache, _FakeDriveService()
537
527
  )
538
528
 
539
529
  assert sheet_id == 1
@@ -166,6 +166,11 @@ class TestAuthStatusResource:
166
166
  assert "create_spreadsheet" in quota["tools"]
167
167
  assert "transfer_ownership" not in quota["tools"]
168
168
  assert "storage quota" in quota["reason"].lower()
169
+ # sync_folder is listed by exact name, with its direction caveat in the
170
+ # reason text (#516); guard the caveat against a reword dropping it (#526).
171
+ assert "sync_folder" in quota["tools"]
172
+ assert "sync_folder" in quota["reason"]
173
+ assert "upload and bidirectional" in quota["reason"]
169
174
  assert quota["alternatives"] is not None
170
175
 
171
176
  def test_service_account_transfer_ownership_limitation(self):