mcp-gee-sweet 0.9.1.dev206__tar.gz → 0.9.2.dev215__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 (242) hide show
  1. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/dev.md +2 -1
  2. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/CLAUDE.md +1 -0
  3. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/PKG-INFO +1 -1
  4. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/gmail-test-plan.md +3 -2
  5. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_transfer.md +91 -1
  6. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/gmail.md +30 -5
  7. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/roadmap.md +21 -10
  8. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/tools.md +2 -2
  9. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/gmail.py +61 -25
  10. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_gmail.py +128 -1
  11. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/uv.lock +3 -3
  12. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/cleanup-worktrees.md +0 -0
  13. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/merge-pr.md +0 -0
  14. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/notify-kai.md +0 -0
  15. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/notify-partner.md +0 -0
  16. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/orchestrator.md +0 -0
  17. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/prep-for-pr.md +0 -0
  18. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/qa-kickoff.md +0 -0
  19. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/release.md +0 -0
  20. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/retro.md +0 -0
  21. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/roadmap-status.md +0 -0
  22. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/start-worktree.md +0 -0
  23. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/team-member.md +0 -0
  24. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/verify-pr.md +0 -0
  25. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/settings.json +0 -0
  26. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/amy.md +0 -0
  27. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/aziz.md +0 -0
  28. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/bob.md +0 -0
  29. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/joy.md +0 -0
  30. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/kai.md +0 -0
  31. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/qa.md +0 -0
  32. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.dockerignore +0 -0
  33. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  35. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/dependabot.yml +0 -0
  37. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/ci.yml +0 -0
  38. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/docs.yml +0 -0
  39. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/publish-dev.yml +0 -0
  40. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/release.yml +0 -0
  41. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.gitignore +0 -0
  42. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.pre-commit-config.yaml +0 -0
  43. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/.python-version +0 -0
  44. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/CHANGELOG.md +0 -0
  45. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/CODE_OF_CONDUCT.md +0 -0
  46. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/CONTRIBUTING.md +0 -0
  47. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/Dockerfile +0 -0
  48. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/LICENSE +0 -0
  49. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/Makefile +0 -0
  50. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/README.md +0 -0
  51. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/SECURITY.md +0 -0
  52. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docker-compose.yml +0 -0
  53. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/assets/logo.png +0 -0
  54. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/auth.md +0 -0
  55. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/client-setup.md +0 -0
  56. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/configuration.md +0 -0
  57. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-async-tool-execution.md +0 -0
  58. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  59. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-cache-invalidation.md +0 -0
  60. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-chart-theming.md +0 -0
  61. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-comments-first-class.md +0 -0
  62. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-composite-tools.md +0 -0
  63. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  64. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  65. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-dev-team.md +0 -0
  66. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-docs-formatting.md +0 -0
  67. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-fork.md +0 -0
  68. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-gmail-domain.md +0 -0
  69. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  70. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  71. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  72. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  73. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-prompt-qa-role.md +0 -0
  74. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-publishing.md +0 -0
  75. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-release-docs-roles.md +0 -0
  76. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-repositioning.md +0 -0
  77. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  78. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  79. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  80. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  81. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-testing.md +0 -0
  82. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/index.md +0 -0
  83. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/blockquote-representation.md +0 -0
  84. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/borderless-table-columns.md +0 -0
  85. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/doc-to-markdown.md +0 -0
  86. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/docs-ast-pipeline.md +0 -0
  87. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/effectiveformat-spike.md +0 -0
  88. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/heading-anchor-resolution.md +0 -0
  89. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/image-conversion.md +0 -0
  90. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/index.md +0 -0
  91. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/markdown-support.md +0 -0
  92. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/module-history.md +0 -0
  93. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/phase3-theme-system.md +0 -0
  94. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design/sse-consent-off-event-loop.md +0 -0
  95. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/design.md +0 -0
  96. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/index.md +0 -0
  97. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/known-limitations.md +0 -0
  98. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/notes-read.md +0 -0
  99. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/README.md +0 -0
  100. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  101. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  102. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  103. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  104. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  105. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  106. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  107. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  108. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  109. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  110. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  111. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  112. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  113. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  114. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  115. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  116. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  117. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  118. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures.template.md +0 -0
  119. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/operations.yaml +0 -0
  120. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/playwright_oauth.md +0 -0
  121. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/.gitkeep +0 -0
  122. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-02.md +0 -0
  123. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-14.md +0 -0
  124. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-17.md +0 -0
  125. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-18.md +0 -0
  126. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  127. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  128. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-28.md +0 -0
  129. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-docs.md +0 -0
  130. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  131. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-infra.md +0 -0
  132. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  133. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04.md +0 -0
  134. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-09-04.md +0 -0
  135. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/retro-v0.8.0.md +0 -0
  136. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/retro-v0.9.0.md +0 -0
  137. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/run.md +0 -0
  138. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/README.md +0 -0
  139. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/v0.8.0.md +0 -0
  140. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/v0.8.1.md +0 -0
  141. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/v0.9.0.md +0 -0
  142. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/setup.md +0 -0
  143. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/calendar.md +0 -0
  144. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_content.md +0 -0
  145. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_layout.md +0 -0
  146. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_style.md +0 -0
  147. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_tables.md +0 -0
  148. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_activity.md +0 -0
  149. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_files.md +0 -0
  150. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_sharing.md +0 -0
  151. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/infra.md +0 -0
  152. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_charts.md +0 -0
  153. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_mgmt.md +0 -0
  154. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_read.md +0 -0
  155. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_write.md +0 -0
  156. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/docs/style-guide.md +0 -0
  157. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/mkdocs.yml +0 -0
  158. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/pyproject.toml +0 -0
  159. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/pyrightconfig.json +0 -0
  160. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/check_supports_all_drives.py +0 -0
  161. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/gen_tool_docs.py +0 -0
  162. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/lane_context_hook.py +0 -0
  163. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/oauth_setup.py +0 -0
  164. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/qa_gmail_fixtures.py +0 -0
  165. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/setup_team.sh +0 -0
  166. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/scripts/write_team_mcp_configs.sh +0 -0
  167. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/server.json +0 -0
  168. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/.env.template +0 -0
  169. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/__init__.py +0 -0
  170. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/auth.py +0 -0
  171. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/cache.py +0 -0
  172. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/http_transport.py +0 -0
  173. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/server.py +0 -0
  174. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  175. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/cache.py +0 -0
  176. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  177. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  178. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  179. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  180. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  181. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
  182. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  183. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  184. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  185. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  186. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  187. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  188. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  189. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  190. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  191. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  192. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  193. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  194. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  195. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  196. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  197. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  198. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  199. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  200. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  201. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  202. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  203. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  204. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  205. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/__init__.py +0 -0
  206. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/conftest.py +0 -0
  207. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/__init__.py +0 -0
  208. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_activity.py +0 -0
  209. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_files.py +0 -0
  210. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_sharing.py +0 -0
  211. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_transfer.py +0 -0
  212. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/integration/__init__.py +0 -0
  213. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/integration/conftest.py +0 -0
  214. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/integration/test_local_fs_harness.py +0 -0
  215. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/__init__.py +0 -0
  216. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/test_data.py +0 -0
  217. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/test_helpers.py +0 -0
  218. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/test_structure.py +0 -0
  219. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_auth.py +0 -0
  220. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_cache.py +0 -0
  221. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_calendar.py +0 -0
  222. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_check_supports_all_drives.py +0 -0
  223. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_concurrency.py +0 -0
  224. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_anchors.py +0 -0
  225. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_comments.py +0 -0
  226. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_content.py +0 -0
  227. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_core.py +0 -0
  228. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_editing.py +0 -0
  229. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_images.py +0 -0
  230. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_indices.py +0 -0
  231. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_layout.py +0 -0
  232. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_markdown_export.py +0 -0
  233. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_named_ranges.py +0 -0
  234. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_style.py +0 -0
  235. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_tables.py +0 -0
  236. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_gen_tool_docs.py +0 -0
  237. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_lane_context_hook.py +0 -0
  238. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_qa_gmail_fixtures.py +0 -0
  239. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_response_limits.py +0 -0
  240. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_server.py +0 -0
  241. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_server_json.py +0 -0
  242. {mcp_gee_sweet-0.9.1.dev206 → mcp_gee_sweet-0.9.2.dev215}/tests/test_team_mcp_configs.py +0 -0
@@ -12,7 +12,7 @@ Branch naming for this slot always puts `<name>` as the **second** `/`-separated
12
12
 
13
13
  - **On `team/<name>` (idle):** this slot is free. This slot's lane label is `lane-a` for Ash, `lane-b` for Jay. Get the queue, filtered to that label: `gh issue list --repo khuisman/mcp-gee-sweet --label "ready-for-development,<lane-label>" --state open --json number,title,body,labels`. Filter out any issue number already claimed by an *open* PR anywhere in the repo (so Ash and Jay never grab the same ticket, though the lane label should already prevent that): `gh pr list --state open --json headRefName --jq '[.[].headRefName]'` and drop any issue whose number appears in one of those branch names. Take the lowest-numbered remaining issue — check its comment thread, not just its body, before treating the body as the current scope: a maintainer triage comment can re-scope or split an issue after filing without editing the original text (e.g. #316 was narrowed to just its concurrency bug and had its progress-notification ask split into a separate backlog issue, #319, entirely via a comment — a session that only reads the issue body and implements straight through will silently redo work that's already been descoped or moved elsewhere). Scope can also go stale with *no* comment at all: if the body cross-references a sibling issue (a "Related" section, "see also #N"), check whether that sibling has already merged — its shipped shape may have generalized past what this issue's own body still literally proposes (#211 proposed a `convert_markdown` param mirroring #188's *original*, narrower proposal on `upload_local_file`; #188 shipped first with a broader `convert: bool` covering CSV/XLSX/DOCX/MD/HTML/PPTX, so implementing #211 exactly as filed would have bolted on a redundant second markdown-conversion switch instead of just wiring `sync_folder` into the mechanism #188 already built). **Stop here and name that issue to the user; don't create a branch or edit files until the user tells you, in this session, to start that ticket.** Bootstrapping via `/team-member`, a peer's `SendMessage` wake, a just-merged PR, or a finished `/retro` is not that instruction: on 2026-09-27 a session bootstrapped only to review open PRs started #511 on its own, leaving uncommitted work nobody had asked for. Once told, then:
14
14
  1. Pick `<type>` to match the ticket (feature vs. fix vs. chore/docs), then `git fetch origin develop && git checkout --no-track -b <type>/<name>/issue-<n> origin/develop`. Keep `--no-track`: without it the ticket branch's upstream is `origin/develop`, so a bare `git pull` merges `develop` in and a push under a non-default `push.default` can land WIP on `develop` (found 2026-09-27 on `chore/jay/issue-511`). Push with `git push -u origin <branch>` at step 6 to set the right upstream.
15
- 2. Work through the issue fully — code, tests, doc changes. When adding a tool to a file with existing siblings that share a parameter (e.g. every `sheet`-taking tool in `structure.py` calls `_get_sheet_id` first and returns a clean `{"error": ...}` for a bad name), grep for that shared pattern across *every* sibling in the file before finishing, not just the one or two tools used as an implementation reference — `get_data_validation` shipped without the check despite ~22 sibling tools having it, caught by QA instead of before opening the PR (PR #361, TC-S100). Separately: converting a sequential loop to concurrent `asyncio.gather` fan-out can silently break implicit ordering-based safety the sequential version had for free (e.g. an existence/dedup check computed once against a snapshot of pre-batch state, safe when items were handled one at a time, unsafe once they all race against that same stale snapshot at once) — audit a sequential block for order-dependent assumptions before parallelizing it, don't just port the body into a gather (PR #351, TC-D200: `download_folder`'s new concurrent fan-out let two same-named Drive files race to write the same local path, something the old sequential loop was accidentally safe against).
15
+ 2. Work through the issue fully — code, tests, doc changes. When adding a tool to a file with existing siblings that share a parameter (e.g. every `sheet`-taking tool in `structure.py` calls `_get_sheet_id` first and returns a clean `{"error": ...}` for a bad name), grep for that shared pattern across *every* sibling in the file before finishing, not just the one or two tools used as an implementation reference — `get_data_validation` shipped without the check despite ~22 sibling tools having it, caught by QA instead of before opening the PR (PR #361, TC-S100). Separately: converting a sequential loop to concurrent `asyncio.gather` fan-out can silently break implicit ordering-based safety the sequential version had for free (e.g. an existence/dedup check computed once against a snapshot of pre-batch state, safe when items were handled one at a time, unsafe once they all race against that same stale snapshot at once) — audit a sequential block for order-dependent assumptions before parallelizing it, don't just port the body into a gather (PR #351, TC-D200: `download_folder`'s new concurrent fan-out let two same-named Drive files race to write the same local path, something the old sequential loop was accidentally safe against). Scope is every problem the issue body and its triage comments describe, not just the one its "Suggested fix" covers. PR #877: #789 described a leaked share *and* a caller left with no file IDs. The suggested `finally` fixed only the first, and QA sent the second back. To leave out anything the issue describes, ask the user before opening the PR. A note in the PR body doesn't count as asking.
16
16
  3. Run the test suite.
17
17
  4. Run `/prep-for-pr`.
18
18
  5. If the ticket added new `TC-DOC`/`TC-D`-style entries to a shared `docs/qa/tests/*.md` file, re-check numbering isn't stale before pushing: `git fetch origin develop`, then `git show origin/develop:<test-file> | grep -oE "^### <PREFIX>[0-9]+" | sort -V | tail -1` (`<PREFIX>` is the file's ID scheme, e.g. `TC-DOC`, `TC-GM`; `sort -V`, not `sort -n`, which falls back to text order on a `TC-` prefix and reports `TC-DOC99` over `TC-DOC198`; the `^### ` anchor matches only definitions, not cross-references) to confirm the numbers just used are still free. `develop` alone isn't enough: also check every *open* PR that touches the same file, since a PR can reserve IDs long before it merges — `gh pr list --state open --json number,headRefName,files --jq '.[] | select(any(.files[]; .path == "<test-file>")) | "\(.number) \(.headRefName)"'`, then for each such branch, `git fetch origin <branch> && git show origin/<branch>:<test-file> | grep -oE "^### <PREFIX>[0-9]+" | sort -V | tail -1` (PR #829: its TC-GM29 was already claimed by the open #830, which had renumbered the Gmail test plan; caught only by chance). If an open PR reserved the ID for the *same* case you're writing (e.g. a known-defect placeholder your fix supersedes), take its number rather than a fresh one, and say in your PR which version should win when the second one merges. These files are append-only and shared across both lanes — another PR can claim the same next number while this ticket is in progress, and a collision usually isn't visible until a reviewer's `gh pr view` shows `mergeable: CONFLICTING` (see PR #337). If a collision is later caught by a reviewer instead of here: rebase onto `origin/develop`, resolve by hand rather than blindly taking one side, renumber/rename consistently across every cross-reference in the file, force-push with `--force-with-lease`, and reply on the PR stating exactly what changed — the full playbook (written after the same friction hit Aziz on PR #338) lives in `.claude/team-roles/aziz.md`'s "Ad-hoc deep-dive QA" section.
@@ -44,6 +44,7 @@ Friction Dev (Ash/Jay) typically hits after finishing a ticket, and where it goe
44
44
  - **Same class, for a claimed invariant rather than an exclusion list.** PR #829 (issue #825), round 2 added a pre-fetch size check resting on "a body's byte size is a lower bound on its serialized size" — reasoned out for UTF-8 and single-byte charsets only, then applied to every charset. QA's next round showed UTF-16/32 and iso-2022-jp break it (a false refusal, confirmed live), costing a full round. Before shipping logic that relies on a property holding over an open input category (charsets, codecs, MIME types, locales), check it against the category's full member list — for codecs, every `codecs.lookup()` name in `encodings.aliases` — not just the members you reasoned about. And don't trust random inputs alone for that check: a random-bytes probe passed `hz` and `iso2022_kr`, whose shift sequences only break the property when repeated. Pair the probe with adversarial inputs, and gate on an explicit allowlist rather than whatever the probe happened to pass.
45
45
  - **A clean (no-conflict) merge of a QA-pushed Result-entry commit can still misattribute content, if your own concurrent fix restructured the same `docs/qa/tests/*.md` section** — distinct from step 19's case above, where `git merge` reports an actual conflict and you're prompted to look. Here there's no conflict at all: confirmed 2026-07-21, PR #386 — QA's review round split `empty_trash` into a design finding, and the fix split one test case (the old single `TC-D204`) into two (`TC-D204` redesigned + a new `TC-D205`). QA had concurrently pushed a commit adding a `**Result (...) held, not run**` note after the *old* `TC-D204`'s Checks block. `git merge origin/<branch> --no-edit` applied that addition via ordinary context-line matching — cleanly, no conflict markers — but because a new heading (`TC-D205`) now sat between the old context lines and where the diff's insertion point landed, the Result note ended up attached under `TC-D205` instead of the case it was actually about. Nothing in the merge output signaled this; it only surfaced by manually re-reading the merged section. Whenever your own fix commit adds, removes, or reorders test-case headings in a shared QA doc file in the same push that also pulls in a QA-authored commit touching that file, re-read the merged section by hand afterward — a clean merge is not proof the content landed in the right place.
46
46
  - **This session's own PR gets merged mid-retro** — in a fast-moving team session, Kai can merge this PR (and self-heal this worktree back onto `develop`) while this session is still running the retro pass on it. A "quick direct fix" commit (e.g. a `CLAUDE.md` command-decision edit) made without re-checking branch state first lands as an orphan on top of the post-merge `develop`, and a plain `git push` of it can even resurrect the just-deleted branch name as a disconnected new branch (confirmed 2026-07-17, PR #353: `git push` reported `[new branch]` for a branch that had a merged, deleted PR moments earlier). Before any direct-commit command decision during retro, run `git log --oneline -3` first — if the ticket's merge commit is already an ancestor, the fix needs `git cherry-pick <stray-sha>` onto a fresh branch cut from current `origin/develop` per `merge-pr.md`'s stray-commit rescue guidance, not a plain commit-and-push on the (now stale) working branch. An empty cherry-pick there means the content already landed through another path (e.g. Kai incorporated it directly) — nothing further to do, just clean up the stray branch (`git branch -D`, `git push origin --delete <name>` if it was pushed) and reset this worktree to `team/<name>`.
47
+ - **`uv lock` rewriting markers on packages you didn't touch means your local `uv` differs from the one that wrote the lock.** PR #877: adding one dependency with local `uv` 0.11.8 also rewrote markers on `exceptiongroup`, `httpcore2`, and `uvicorn`. Don't commit that. Re-lock with the current release without changing the installed one: `uvx --from uv@latest uv lock`. That may bump the lockfile's `revision =` header (3 → 5 with 0.12.23, checked 2026-10-04). That's expected, and older `uv` still reads it. Only marker changes on unrelated packages are the problem. If that isn't possible, revert `uv.lock`, add only the project's own lines (its `dependencies` and `requires-dist` entries) by hand, and confirm with `uv lock --check`.
47
48
  - **Ticket-pickup scope can go stale via a merged sibling issue with no comment on the issue itself.** Folded into step 9's queue-pickup guidance above (2026-07-25, #211/#188) — no separate incident detail needed here.
48
49
  - **A sharper variant: the branch switch happens mid-command, not just pre-stale.** Confirmed 2026-07-19, issue #363/PR #368: a scratch branch was checked out (`git checkout -b tmp-.../ origin/develop`) for a second command-decision doc edit, a file was read and edited, and by the time `git add && git commit` ran, `git branch --show-current` had silently become `team/ash` instead — something else had run this exact worktree's self-heal sequence (`git checkout team/<name>` + `git reset --hard origin/develop`) *inside this same worktree directory* while the edit was in flight, most plausibly a second live session bootstrapped into the same slot after seeing the PR merged (this worktree's shared stash-stack warning already establishes that concurrent sessions can touch the same repo; this shows they can touch the *same worktree's checked-out branch* too). There was no error, no conflict — the commit just landed silently on the wrong branch. The same cherry-pick recovery above still applies (it doesn't care which branch the stray commit ended up on), but the lesson is upstream of that: during a retro pass with more than one scratch-branch checkout in flight, re-run `git branch --show-current` immediately before every `git add`/`git commit`, not just before the first one — don't assume HEAD is still where the last `git checkout` left it once real wall-clock time (a file read, an edit) has passed.
49
50
  - **A newly-introduced cross-cutting mechanism must be audited against every code path a docstring claims shares it — the claim itself isn't proof it's kept in sync.** PR #414 (issue #211), round 2: the fix for round 1's matching bug added a Drive `properties` marker stamped by `sync_folder`'s own conversion `create()` call. `upload_local_file`'s `convert=True` path creates the exact same kind of converted Doc, and its own docstring calls this "the same mechanism" — but the marker-stamping fix only touched `sync_folder`'s call site, leaving `upload_local_file`'s converted Docs invisible to `sync_folder`'s matching and silently duplicated on the next sync. Caught by QA, not before opening the PR. When a fix introduces a new marker, flag, or stamped property to make one code path recognize an artifact, grep for every other function whose docstring or comments assert equivalence ("same mechanism as X", "same as Y's Z param") and verify each one independently — don't take the shared docstring's word for it.
@@ -121,6 +121,7 @@ Set `AUTH_METHOD` to pin a specific method with no fallback — removes ambiguit
121
121
  - Wrap every `.execute()` as `await execute_in_thread(chain.execute, service)`. Never write `asyncio.to_thread(chain.execute, http=thread_http(service))`: the kwarg is evaluated on the event-loop thread, so concurrent calls share one non-thread-safe `httplib2` transport.
122
122
  - Six multi-call tools (`sync_folder`, `get_multiple_sheet_data`, `get_multiple_spreadsheet_summary`, `import_csv_to_sheet`, `share_spreadsheet`, `share_file`) fan out with `asyncio.gather(..., return_exceptions=True)`.
123
123
  - A `ctx.report_progress()` call inside a gathered per-item coroutine must be wrapped in its own try/except, or a dropped client session turns an already-successful item into a failure.
124
+ - Cleanup that must run when a tool call is cancelled (revoking a temporary share, deleting a temp file) needs `with anyio.CancelScope(shield=True):`, not just a `finally`. mcp cancels a request through an anyio cancel scope, which re-delivers the cancellation at every `await`, including the cleanup's own `await` inside `finally`. Shield the step that creates the resource too, or one created mid-`gather` is never recorded for cleanup. Keep shielded work bounded: a cancel waits for it. `_apply_doc_content` is the reference (PR #877).
124
125
  - `cache.py`'s SQLite I/O stays synchronous on purpose (see `cache.py::_open()`). `_BaseCache.snapshot_epoch()`/`store(..., epoch=...)` stop a mid-fetch `refresh_cache()` from being overwritten, per key (`SheetStructureCache`, `SheetDataCache`, `DriveFolderCache`). Size-bounded caches share `_get_sized`/`_store_sized`.
125
126
  - To unit-test real cross-thread concurrency, put the barrier inside the mocked `.execute()`, not `.list()`/`.create()`, which run on the event-loop thread (see `tests/drive/test_transfer.py::TestSyncFolderRecursive::test_recursive_sibling_subfolders_descend_concurrently`).
126
127
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gee-sweet
3
- Version: 0.9.1.dev206
3
+ Version: 0.9.2.dev215
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
@@ -73,7 +73,7 @@ Add `scripts/qa_gmail_fixtures.py` (new), modeled on the TC-GM23 setup snippet:
73
73
  |---|---|---|---|---|
74
74
  | `plain` | Sent | Single `text/plain` from the sender mailbox; lands `INBOX`+`UNREAD` | `TEST_MESSAGE_ID` | GM01, 03, 19 |
75
75
  | `alt-unicode` | Sent | `body` + `body_html`, non-ASCII subject and body, astral emoji | `TEST_GMAIL_UNICODE_ID` | GM27 |
76
- | `attachments` | Sent + Inserted | Sent: body + small PDF + CSV. Inserted: a `multipart/related` inline PNG with `Content-ID` and **no filename** (`send_message` can't build `related`) | `TEST_GMAIL_ATTACH_ID`, `TEST_GMAIL_INLINE_ID` | GM28 |
76
+ | `attachments` | Sent + Inserted | Sent: body + small PDF + CSV. Inserted: a `multipart/related` inline PNG with `Content-ID` and **no filename** (`send_message` can't build `related`) | `TEST_GMAIL_ATTACH_ID`, `TEST_GMAIL_INLINE_ID` | GM28, 46 |
77
77
  | `thread` | Sent (both sides) | Sender sends; the QA mailbox replies; the sender replies again. A real two-party, 3-message thread with Gmail-generated `Message-ID`/`References` | `TEST_THREAD_ID` | GM05, 07, 33, 43 |
78
78
  | `reply-to` | Inserted | Foreign `From: noreply@example.invalid`, `Reply-To` = `+tc-gm23` (`send_message` has no `Reply-To` param) | `TEST_GMAIL_REPLYTO_ID` | GM23 (replaces its inline setup) |
79
79
  | `forwarded` | Sent | Outer body, then a forwarded message as a real `message/rfc822` part named `forwarded.eml` | `TEST_GMAIL_FWD_ID` | GM29 |
@@ -112,7 +112,7 @@ Keep the IDs and intent. Rewrite each to name the exact tool and params and to r
112
112
  | GM28 | `get_message` | Both attachments (PDF, CSV) listed with filename, MIME type, size, `attachment_id`. The inline PNG (`inline` fixture) is listed with `filename: null`; Gmail stores it by `attachmentId` (§5, P3: not a defect). | read |
113
113
  | GM29 | `get_message` | `body_plain` is the *outer* message's body; the `.eml` is listed as a `message/rfc822` attachment (§5, P2: not a defect with Gmail's real layout) | read |
114
114
  | GM30 | `get_message`, `get_thread` | `large-body` (Gmail delivers both 3 MB parts by `attachmentId`, #825). Without `local_path`: the size-cap error, naming `local_path`, since ~6 MB exceeds the default cap. With `local_path`: the written JSON has `body_plain` (3,000,002 chars) and `body_html` (3,037,987 chars) filled, `attachments: []`, no `body_fetch_errors`. Same for `get_thread` on its thread. Written in the #825 PR (#829). | read |
115
- | GM31 | `get_thread` | Over-cap thread returns the size-cap error, not a dropped connection or a truncated body. Captures #793's motivating failure live. | read |
115
+ | GM31 | `get_thread` | Over-cap thread returns the size-cap error, not a dropped connection or a truncated body, and the error points at `include_body=False`. With `include_body=False` the same thread lists its 3 message IDs and headers with no bodies (#793). | read |
116
116
  | GM32 | `get_message` | Latin-1 body decodes correctly. A zero-setup regression companion to TC-GM26: #792 closed with the finding that Gmail transcodes every text part to UTF-8, so this is expected to **pass**. | read |
117
117
  | GM33 | `get_thread` | 3 messages in chronological order, each shaped like `get_message`, `in_reply_to`/`references` populated | read |
118
118
  | GM34 | `list_messages`, `list_threads` | `max_results=3` over the 7 `page` fixtures: `next_page_token` present, page 2 has no overlap with page 1, final page has no token. Also `max_results=0` and `9999` clamp without error. | read |
@@ -127,6 +127,7 @@ Keep the IDs and intent. Rewrite each to name the exact tool and params and to r
127
127
  | GM43 | `reply_to_message` | Reply into the 3-message `thread` from its middle message: `references` chains all prior IDs, reply lands in the same thread. M2 was sent by the QA mailbox, so the reply is delivered to the sender mailbox | write, cross-mailbox |
128
128
  | GM44 | `reply_to_message` (QA) → `list_messages` (sender) | End-to-end delivery. The QA mailbox replies to the `plain` fixture. On the sender side, `list_messages(query='subject:"[mcp-qa:plain]"')` shows the reply arrived in the **same thread** as the original. Proves `In-Reply-To`/`References` thread correctly in the recipient's mailbox, not just in ours. Plus-address cases can't show this. | write, cross-mailbox |
129
129
  | GM45 | `send_message` (QA) → `get_message` (sender) | QA sends to the sender mailbox with `cc` = a QA plus-address and `bcc` = another. On the sender side, `get_message` shows `cc` present and **no** `bcc` header. Bcc must be stripped from delivered copies; GM39 only sees the sender's copy. | write, cross-mailbox |
130
+ | GM46 | `get_message` | `include_body=False` on the `attachments` fixture: headers, snippet, labels, and `size_estimate` present; no `body_plain`, `body_html`, or `attachments` key, since Gmail's `metadata` format returns no payload parts (#793) | read |
130
131
 
131
132
  Every write case uses a `+tc-gmNN` plus-address and an `[mcp-qa:tc-gmNN]` subject (so `reset` sweeps it), and ends with a **Cleanup** step.
132
133
 
@@ -546,6 +546,8 @@ download {DOC_ID} with no export_format: ValueError "export_format is required f
546
546
  **Result (2026-09-04) ✅ PASS**
547
547
  No PNG fixture — used qa-upload.txt → local_path="/tmp/qa-specific-name.txt": written to exactly that path (not a subdir), content "Hello from QA" intact, 13 bytes. Parent dir already existed.
548
548
 
549
+ **Result (2026-10-03) ✅ PASS** — re-run after #855's destination preflight, via an isolated service-account sandbox. No PNG fixture — used a 25-byte `beta.bin` → `local_path=".../newparent/deeper/exact-name.bin"` (neither `newparent/` nor `deeper/` existing): written to exactly that path, content intact, both parent levels created. A second call to an existing regular file (`.../existing.bin`, content `old`) replaced it with the downloaded 15 bytes, as the preflight's docstring allows.
550
+
549
551
  ---
550
552
 
551
553
  ### TC-D254: `download_file` — a non-existent trailing-slash `local_path` is created as a directory, not written as a file (issue #690) ⚠️ local-filesystem
@@ -562,7 +564,7 @@ No PNG fixture — used qa-upload.txt → local_path="/tmp/qa-specific-name.txt"
562
564
  **Checks**
563
565
  - Step 1: `/tmp/qa-d254/new/sub/` exists as a **directory**; the file is at `/tmp/qa-d254/new/sub/<drive_name>` with the real content; the returned `local_path` is that full file path. There is **no** plain file at `/tmp/qa-d254/new/sub`.
564
566
  - Step 2: both downloads are present as separate files inside `/tmp/qa-d254/new/sub/` (named after each Drive file) — the second did not overwrite the first, and `/tmp/qa-d254/new/sub` is still a directory, not a file.
565
- - Step 3: the call returns/raises a `ValueError` mentioning a non-directory already exists at that path; `/tmp/qa-d254/clash` still contains `x`, untouched.
567
+ - Step 3: the call raises a `ValueError` `local_path '/tmp/qa-d254/clash/' has a non-directory path component at '/tmp/qa-d254/clash'` (wording since #855; before that it read "…ends in a path separator (implying a directory) but a non-directory already exists at…"); `/tmp/qa-d254/clash` still contains `x`, untouched.
566
568
 
567
569
  **Teardown**
568
570
  `rm -rf /tmp/qa-d254`.
@@ -574,6 +576,94 @@ No PNG fixture — used qa-upload.txt → local_path="/tmp/qa-specific-name.txt"
574
576
 
575
577
  Unit tests: `tests/drive/test_transfer.py -k TestDownloadFile` — 10 passed (incl. the 3 new `test_trailing_slash_*` cases). `/code-review high origin/develop...HEAD` surfaced 3 non-blocking cleanups (fail-fast ordering of the clash `ValueError` vs. the `files().get()` call; a dead `os.altsep` clause in the `wants_dir` test; a raw `NotADirectoryError` when an *intermediate* parent is a regular file) — none blocking, filed as follow-up #724.
576
578
 
579
+ **Result (2026-10-03) ✅ PASS** — regression re-run after #827 and #855, via an isolated service-account sandbox on a Shared Drive. Fixtures: `alpha.bin` (15 B) and `beta.bin` (25 B) uploaded to the sandbox drive root, removed at teardown.
580
+ - **Step 1:** `.../new/sub/` created as a directory (both levels); returned `local_path` `/tmp/qa-d254/new/sub/alpha.bin`, 15 B, content intact.
581
+ - **Step 2:** `beta.bin` (25 B) written next to `alpha.bin`, which was unchanged; `.../new/sub` still a directory.
582
+ - **Step 3:** raised `ValueError` `local_path '/tmp/qa-d254/clash/' has a non-directory path component at '/tmp/qa-d254/clash'`; `clash` still contains `x`.
583
+
584
+ ---
585
+
586
+ ### TC-D274: `download_file` — trailing-slash `local_path` over an existing regular file fails before any Drive request (#827, #855) ⚠️ local-filesystem
587
+
588
+ **Background:** #827 moved the trailing-slash clash check ahead of the Drive metadata call, and #855 folded it into a single preflight (`_validate_local_destination`). A bad destination should now fail without spending a Drive request. A file ID that doesn't exist is the live stand-in for "no Drive request": if the call reaches Drive it gets a 404 `HttpError`, and if the preflight rejects it first the path error comes back.
589
+
590
+ **Setup:** `rm -rf /tmp/qa-d274 && mkdir -p /tmp/qa-d274 && printf x > /tmp/qa-d274/clash`
591
+
592
+ **Prompt**
593
+ > 1. Control: `download_file(file_id="qa-bogus-file-id", local_path="/tmp/qa-d274/ok/")`
594
+ > 2. `download_file(file_id="qa-bogus-file-id", local_path="/tmp/qa-d274/clash/")`
595
+
596
+ **Checks**
597
+ - Step 1 raises the Drive 404 `HttpError` ("File not found: qa-bogus-file-id"), which shows a valid destination does reach Drive. `/tmp/qa-d274/ok/` is **not** created, because mkdir runs after the metadata call.
598
+ - Step 2 raises `ValueError` `local_path '/tmp/qa-d274/clash/' has a non-directory path component at '/tmp/qa-d274/clash'`, with **no** 404. `/tmp/qa-d274/clash` still contains `x`.
599
+
600
+ **Teardown**
601
+ `rm -rf /tmp/qa-d274`.
602
+
603
+ **Result (2026-10-03) ✅ PASS** — via an isolated service-account sandbox (PR #855), with a job-scoped local directory standing in for `/tmp/qa-d274`. Step 1: `HttpError 404 … "File not found: pr855-bogus-file-id."`, and the `control/` directory was not created. Step 2: `ValueError` `local_path '…/clash/' has a non-directory path component at '…/clash'`, no 404, and `clash` still contained `x`.
604
+
605
+ ---
606
+
607
+ ### TC-D275: `download_file` — trailing-slash `local_path` whose intermediate parent is a regular file raises `ValueError`, not `NotADirectoryError` (#827, #855) ⚠️ local-filesystem
608
+
609
+ **Background:** Before #827, `.../clash/sub/` with `clash` a regular file leaked a raw `NotADirectoryError` from `mkdir`. Uses the same bogus-ID approach as TC-D274.
610
+
611
+ **Setup:** `rm -rf /tmp/qa-d275 && mkdir -p /tmp/qa-d275 && printf x > /tmp/qa-d275/clash`
612
+
613
+ **Prompt**
614
+ > `download_file(file_id="qa-bogus-file-id", local_path="/tmp/qa-d275/clash/sub/")`
615
+
616
+ **Checks**
617
+ - Raises `ValueError` `local_path '/tmp/qa-d275/clash/sub/' has a non-directory path component at '/tmp/qa-d275/clash'`. The message names the blocking file `clash`, not `sub`.
618
+ - No `NotADirectoryError` and no Drive 404.
619
+ - `/tmp/qa-d275/clash` still contains `x`.
620
+
621
+ **Teardown**
622
+ `rm -rf /tmp/qa-d275`.
623
+
624
+ **Result (2026-10-03) ✅ PASS** — via an isolated service-account sandbox (PR #855). Raised `ValueError` `local_path '…/clash/sub/' has a non-directory path component at '…/clash'`, with no `NotADirectoryError` and no 404. `clash` still contained `x`.
625
+
626
+ ---
627
+
628
+ ### TC-D276: `download_file` — plain file `local_path` whose intermediate parent is a regular file fails before any Drive request (#836, #855) ⚠️ local-filesystem
629
+
630
+ **Background:** #836: without a trailing slash, the destination's *parent* chain gets checked. Before #855, `.../clash/sub/out.bin` spent a Drive metadata request and then leaked `NotADirectoryError` from the parent `mkdir`.
631
+
632
+ **Setup:** `rm -rf /tmp/qa-d276 && mkdir -p /tmp/qa-d276 && printf x > /tmp/qa-d276/clash`
633
+
634
+ **Prompt**
635
+ > `download_file(file_id="qa-bogus-file-id", local_path="/tmp/qa-d276/clash/sub/out.bin")`
636
+
637
+ **Checks**
638
+ - Raises `ValueError` `local_path '/tmp/qa-d276/clash/sub/out.bin' has a non-directory path component at '/tmp/qa-d276/clash'`.
639
+ - No Drive 404 and no `NotADirectoryError`.
640
+ - `/tmp/qa-d276/clash` still contains `x`.
641
+
642
+ **Teardown**
643
+ `rm -rf /tmp/qa-d276`.
644
+
645
+ **Result (2026-10-03) ✅ PASS** — via an isolated service-account sandbox (PR #855). Raised `ValueError` `local_path '…/clash/sub/out.bin' has a non-directory path component at '…/clash'`, with no 404. `clash` still contained `x`.
646
+
647
+ ---
648
+
649
+ ### TC-D277: `download_file` — trailing-slash `local_path` that is a dangling symlink names the symlink and fails before any Drive request (#836, #855) ⚠️ local-filesystem
650
+
651
+ **Background:** `Path.exists()` is false for a dangling symlink, but `mkdir` still trips over it. #855's preflight also checks `is_symlink()` so this case gets caught.
652
+
653
+ **Setup:** `rm -rf /tmp/qa-d277 && mkdir -p /tmp/qa-d277 && ln -s /tmp/qa-d277/missing-target /tmp/qa-d277/dangling`
654
+
655
+ **Prompt**
656
+ > `download_file(file_id="qa-bogus-file-id", local_path="/tmp/qa-d277/dangling/")`
657
+
658
+ **Checks**
659
+ - Raises `ValueError` `local_path '/tmp/qa-d277/dangling/' has a non-directory path component at '/tmp/qa-d277/dangling'`, with no Drive 404.
660
+ - The symlink is unchanged, and `/tmp/qa-d277/missing-target` was **not** created.
661
+
662
+ **Teardown**
663
+ `rm -rf /tmp/qa-d277`.
664
+
665
+ **Result (2026-10-03) ✅ PASS** — via an isolated service-account sandbox (PR #855). Raised `ValueError` `local_path '…/dangling/' has a non-directory path component at '…/dangling'`, with no 404. The symlink was intact and `missing-target` did not exist afterwards.
666
+
577
667
  ---
578
668
 
579
669
  ## `download_folder`
@@ -333,6 +333,24 @@ Record the printed ID as `{UTF16_ID}`.
333
333
 
334
334
  ---
335
335
 
336
+ ### TC-GM46: `include_body: false` returns headers without body or attachments (issue #793)
337
+
338
+ **Background:** `include_body: false` fetches with Gmail's `format='metadata'`, which returns headers but no payload parts (confirmed live 2026-10-04). So the result omits attachment metadata as well as the body.
339
+
340
+ **Action**
341
+ 1. `get_message` with `message_id: "{TEST_GMAIL_ATTACH_ID}"`, `include_body: false`
342
+
343
+ **Checks**
344
+ - `id` is `{TEST_GMAIL_ATTACH_ID}`; `thread_id`, `snippet`, `label_ids`, `internal_date`, and a positive `size_estimate` are present
345
+ - `headers.subject` starts `[mcp-qa:`, `headers.from` contains `{SENDER}`, `headers.message_id` is non-empty
346
+ - No `body_plain`, `body_html`, `attachments`, `body_decode_errors`, `body_fetch_errors`, or `error` key
347
+
348
+ **Result** (2026-10-04, PR #885 round 1 at `47937d8`, `mcp-gee-sweet-kit`, OAuth token with `gmail.modify`): **PASS**. `id`, `thread_id`, `snippet` (`mcp-gee-sweet attachments fixture: a PDF and a CSV.`), `label_ids`, `internal_date` and `size_estimate` = 7893 present. `headers.subject` = `[mcp-qa:attachments] PDF + CSV`, `headers.from` = `{SENDER}`, `headers.message_id` non-empty. No body, attachment, error-list or `error` keys. The PR still went back to the Dev for code-review findings this case doesn't exercise; see the PR #885 comment.
349
+
350
+ **Result** (2026-10-04, PR #885 round 2 at `48bf23f`, `mcp-gee-sweet-kit`, OAuth token with `gmail.modify`): **PASS (regression)**. The output matches round 1 field for field. The fix's `metadataHeaders` filter can't be seen through shaping, so it was checked against the raw API instead (see TC-GM31's round 2 result).
351
+
352
+ ---
353
+
336
354
  ## `list_threads`
337
355
 
338
356
  ### TC-GM05: List the fixture threads
@@ -381,18 +399,25 @@ Record the printed ID as `{UTF16_ID}`.
381
399
 
382
400
  ---
383
401
 
384
- ### TC-GM31: An over-cap thread returns the size-cap error
402
+ ### TC-GM31: An over-cap thread returns the size-cap error, and `include_body: false` lists it (issue #793)
385
403
 
386
- **Background:** the `over-cap-thread` fixture is 3 messages of ~400 KB each, which is over the default `MAX_TOOL_RESPONSE_CHARS` in full format but under it per message. This is the failure #793 wants a lighter option for. Today the tool should refuse cleanly rather than drop the connection or truncate.
404
+ **Background:** the `over-cap-thread` fixture is 3 messages of ~400 KB each, which is over the default `MAX_TOOL_RESPONSE_CHARS` in full format but under it per message. The full fetch should refuse cleanly rather than drop the connection or truncate. Since #793, `include_body: false` fetches only metadata, which is small enough to list the thread's message IDs.
387
405
 
388
406
  **Action**
389
407
  1. `get_thread` with `thread_id: "{TEST_GMAIL_BIG_THREAD_ID}"`
390
- 2. `list_messages` with `query: "label:mcp-qa-fixture subject:(mcp-qa over-cap-thread)"`, then `get_message` on any one returned ID
408
+ 2. `get_thread` with `thread_id: "{TEST_GMAIL_BIG_THREAD_ID}"`, `include_body: false`
409
+ 3. `get_message` with `message_id` set to the first message ID from step 2
391
410
 
392
411
  **Checks**
393
- - Step 1: the tool call fails with an error containing `get_thread: the response is` and `safety cap`, and the error suggests `get_message` on individual IDs. The session stays connected: the next call works.
412
+ - Step 1: the tool call fails with an error containing `get_thread: the response is` and `safety cap`. The error suggests `include_body=False` followed by `get_message` on individual IDs. The session stays connected: the next call works.
394
413
  - Step 1: no partial or truncated thread is returned
395
- - Step 2: 3 messages listed; `get_message` succeeds, with a `body_plain` that starts `mcp-gee-sweet over-cap-thread message`
414
+ - Step 2: no `error`. `id` is `{TEST_GMAIL_BIG_THREAD_ID}`, and `messages` has 3 entries, each with `id`, `thread_id`, `snippet`, `label_ids`, `size_estimate`, and `headers` (`headers.subject` contains `[mcp-qa:over-cap-thread]`)
415
+ - Step 2: no message has a `body_plain`, `body_html`, or `attachments` key, and the whole response is a few KB, not ~1.2 MB
416
+ - Step 3: succeeds, with a `body_plain` that starts `mcp-gee-sweet over-cap-thread message`
417
+
418
+ **Result** (2026-10-04, PR #885 round 1 at `47937d8`, `mcp-gee-sweet-kit`, OAuth token with `gmail.modify`): **PASS**. Step 1: `get_thread: the response is 1275124 characters, over the 1000000-character safety cap`, with the hint naming `include_body=False` then `get_message` on individual IDs; no partial thread, and the next call worked. Step 2: no `error`, 3 messages, each with `id`, `thread_id`, `snippet`, `label_ids`, `size_estimate` (~412 KB each) and `headers` (subjects contain `[mcp-qa:over-cap-thread]`; `in_reply_to`/`references` chain correctly), no body or attachment keys; the response was about 3 KB. Step 3: run with `local_path` to keep ~425 KB out of the QA session's context (424,899 bytes written); `body_plain` starts `mcp-gee-sweet over-cap-thread message 1.`. The thread-level `snippet` is `null`, the same as the full format (`threads.get` returns none).
419
+
420
+ **Result** (2026-10-04, PR #885 round 2 at `48bf23f`, `mcp-gee-sweet-kit`, OAuth token with `gmail.modify`): **PASS**. Steps 1–3 matched round 1 exactly (1,275,124-character cap error with the `include_body=False` hint; 3 metadata-only messages; step 3 via `local_path`, 424,899 bytes). The `metadataHeaders` filter was checked with a fresh-interpreter raw `threads.get` on the same thread using this slot's token. Plain `format=metadata` returned 24,591 bytes and 88 headers (ARC-*, DKIM-Signature, Received, X-Gm-*, and others). `_get_format_kwargs(False)` returned 3,003 bytes and 19 headers, all from the shaped set (Date, From, In-Reply-To, Message-ID, References, Subject, To).
396
421
 
397
422
  ---
398
423
 
@@ -18,10 +18,11 @@ Sheets, Drive, Docs, and Calendar are all covered — see [Tools](tools.md) for
18
18
  | [**v0.8.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.8) | ✅ Tier 1 complete — all "frequently needed" items across all domains (84 tools) | Published 2026-06-29 |
19
19
  | [**v0.8.1**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.8.1) | Defect & documentation cleanup — no new tools, ships the QA/refactor work already on `develop` plus fixes for #235, #242, #213, #239, #236 | Stabilizes before Tier 2 feature work begins |
20
20
  | [**v0.9.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9) | ✅ Tier 2 complete — power-user and structured-work layer, plus defects that surfaced after v0.8.1 shipped ([#248](https://github.com/khuisman/mcp-gee-sweet/issues/248)) | Published 2026-09-08 |
21
- | [**v0.9.1**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.1) | Post-release defect fixes & infrastructure addons, plus the Gmail domain (pulled forward from v1.1+ 2026-09-23, community PR #786) | Stabilizes before Tier 3 begins |
22
- | [**v0.9.2**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.2) | Comments as a first-class, cross-suite capability — generalize + complete the Drive `comments`/`replies` surface ([#661](https://github.com/khuisman/mcp-gee-sweet/issues/661)); ships a breaking rename | Closes out the comments story before Tier 3 |
21
+ | [**v0.9.1**](https://github.com/khuisman/mcp-gee-sweet/releases/tag/v0.9.1) | ✅ Security hotfix cut from v0.9.0 — `download_file`, `download_folder`, and `sync_folder` keep every write inside `local_path`. Nothing else from `develop` | Published 2026-10-04 |
22
+ | [**v0.9.2**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.2) | Post-release defect fixes & infrastructure addons, plus the Gmail domain (pulled forward from v1.1+ 2026-09-23, community PR #786) | Stabilizes before Tier 3 begins |
23
+ | [**v0.9.3**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.3) | Comments as a first-class, cross-suite capability — generalize + complete the Drive `comments`/`replies` surface ([#661](https://github.com/khuisman/mcp-gee-sweet/issues/661)); ships a breaking rename | Closes out the comments story before Tier 3 |
23
24
  | [**v1.0.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av1.0) | API stability declaration — Tier 3 items that make the cut + any breaking cleanups from v0.8–0.9 | Backwards-compatibility commitment |
24
- | [**v1.1.0+**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3A%22v1.1%2B%22) | Future domains — Tasks (separate minor release, needs a new API client; Gmail pulled forward to v0.9.1) | Expanded scope |
25
+ | [**v1.1.0+**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3A%22v1.1%2B%22) | Future domains — Tasks (separate minor release, needs a new API client; Gmail pulled forward to v0.9.2) | Expanded scope |
25
26
 
26
27
  Tier 4 items remain backlog with no assigned version.
27
28
 
@@ -180,7 +181,9 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
180
181
  - [x] `share_spreadsheet` fails with "File not found" on any Shared Drive file — missing `supportsAllDrives=True` on its `permissions().create()` call, the only one in `sharing.py` without it (`ready-for-development`, `lane-b`) ([#687](https://github.com/khuisman/mcp-gee-sweet/issues/687)) (PR #695)
181
182
  - [x] Folder-listing cache key omits `max_results` — silent truncation on a cache hit, and a small-limit fetch poisons the cache for later larger-limit calls (`ready-for-development`, `lane-b`) ([#688](https://github.com/khuisman/mcp-gee-sweet/issues/688)) (PR #697)
182
183
 
183
- ### v0.9.1 — Post-release defect fixes & infrastructure addons _(target: [v0.9.1](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.1), before Tier 3 begins)_
184
+ ### v0.9.2 — Post-release defect fixes & infrastructure addons _(target: [v0.9.2](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.2), before Tier 3 begins)_
185
+
186
+ _Renumbered from v0.9.1 on 2026-10-04: v0.9.1 shipped as a security hotfix cut from v0.9.0, so this release (and the old v0.9.2, now v0.9.3) moved up one. The GitHub labels were renamed to match._
184
187
 
185
188
  Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped, plus a handful of infrastructure/tooling additions that don't belong to any specific tool tier. Started because v0.9.0's own scope kept growing past its original Tier 2 definition; splitting this out keeps that pattern from repeating indefinitely. The one exception to its original "no new tools" scope is the Gmail domain, pulled forward from v1.1+ on 2026-09-23 because a community contributor had already implemented it (see **Gmail domain** below).
186
189
 
@@ -193,6 +196,8 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
193
196
  - [ ] `_timed` logs a `200` status for tool calls that catch their own exception and return `{error: ...}` — observability reports success for a call that actually failed ([#579](https://github.com/khuisman/mcp-gee-sweet/issues/579))
194
197
  - [ ] `emitter.py` table-cell images have no cursor-advance handling — latent infinite loop if that code path is ever reached ([#509](https://github.com/khuisman/mcp-gee-sweet/issues/509))
195
198
  - [ ] Calendar ACL tools use a bare `except Exception` instead of the `HttpError` friendly-error pattern used elsewhere in `calendar.py` ([#459](https://github.com/khuisman/mcp-gee-sweet/issues/459))
199
+ - [ ] `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))
200
+ - [ ] 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 ([#868](https://github.com/khuisman/mcp-gee-sweet/issues/868))
196
201
  - [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))
197
202
  - [x] `sync_folder convert_markdown`'s `modifiedTime` restamp fires for all convert types (over-broad), and its create()+update() pattern is duplicated across two call sites ([#435](https://github.com/khuisman/mcp-gee-sweet/issues/435)) (PR #817)
198
203
  - [ ] Harden `tests/integration`'s local-fs live QA harness — error surfacing on API failures, subprocess/handshake timeouts, cleanup-masking a real test failure, per-test subprocess overhead ([#648](https://github.com/khuisman/mcp-gee-sweet/issues/648))
@@ -239,7 +244,9 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
239
244
  - [ ] `ast_to_markdown.py`: image-render duplication + falsy-based code-block buffer flush ([#600](https://github.com/khuisman/mcp-gee-sweet/issues/600))
240
245
  - [ ] `html_parser.py` `handle_data`: a SIM114 lint merge conflates two structurally distinct buffering triggers ([#638](https://github.com/khuisman/mcp-gee-sweet/issues/638))
241
246
  - [ ] Docs image-sharing failure paths: unrevocable `anyone:reader` grant + missing folder-cache invalidation in `_replace_doc_content` ([#666](https://github.com/khuisman/mcp-gee-sweet/issues/666))
242
- - [ ] `_apply_doc_content`: temporary `anyone:reader` image shares aren't revoked when the doc edit raises — revoke pass should run in a `finally`. Same share/revoke surface as #666 and #511 ([#789](https://github.com/khuisman/mcp-gee-sweet/issues/789))
247
+ - [ ] `write_doc_content`/`update_doc_from_file` leave the doc empty when the replacement fails after the clear — decided 2026-10-04: record the pre-clear revision ID and name it in the error so the caller can recover via `export_revision` ([#878](https://github.com/khuisman/mcp-gee-sweet/issues/878))
248
+ - [ ] `insert_local_images`: temporary `anyone:reader` image shares aren't revoked when the call is cancelled — apply PR #877's shielded-cleanup pattern — lane B ([#883](https://github.com/khuisman/mcp-gee-sweet/issues/883))
249
+ - [x] `_apply_doc_content`: temporary `anyone:reader` image shares aren't revoked when the doc edit raises — revoke pass should run in a `finally`. Same share/revoke surface as #666 and #511 ([#789](https://github.com/khuisman/mcp-gee-sweet/issues/789)) (PR #877)
243
250
  - [ ] Docstrings: `create_paragraph_bullets` carries internal API-quirk mechanics plus residual bare issue refs in `style.py` tool docstrings ([#683](https://github.com/khuisman/mcp-gee-sweet/issues/683))
244
251
  - [ ] `TestIsolatedDepthRunGlyph` asserts request shape, not rendered nesting outcome (PR #711) ([#715](https://github.com/khuisman/mcp-gee-sweet/issues/715))
245
252
  - [ ] `style_doc_range` has no `font_family` parameter — can't apply monospace to an existing range ([#720](https://github.com/khuisman/mcp-gee-sweet/issues/720))
@@ -267,6 +274,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
267
274
  - [x] Deduplicate the `max_results` clamp expression, hand-copied 8x across `drive/files.py` with inconsistent caps (PR #736 follow-up) ([#737](https://github.com/khuisman/mcp-gee-sweet/issues/737)) (PR #781)
268
275
  - [x] `download_file`: one pre-flight `local_path` check before any Drive call, for both trailing-slash and plain branches, naming the blocking path (#724/#827 follow-up; on-deck, `good first issue`) (PR #855) ([#836](https://github.com/khuisman/mcp-gee-sweet/issues/836))
269
276
  - [ ] `sync_folder`: a failed mid-stream download leaves a truncated local file with a fresh mtime, which the next sync uploads over the intact Drive copy — download to a temp file and `os.replace` on success; check `download_file`/`download_folder` too. Found by code review, not yet observed live (lane-a, after #814 since both touch `_run_one`) ([#844](https://github.com/khuisman/mcp-gee-sweet/issues/844))
277
+ - [ ] `download_file` local-path preflight follow-ups (#836/PR #855): mkdir-race fallback, trailing `/.` treated as a file destination, dangling-symlink inconsistency — lane A ([#874](https://github.com/khuisman/mcp-gee-sweet/issues/874))
270
278
 
271
279
  **Calendar hardening** _(triaged out of `backlog` 2026-09-11)_
272
280
  - [ ] `list_calendar_acl` pagination fix (#460) needs a response-size cap, loop safety bound, and partial-failure handling ([#615](https://github.com/khuisman/mcp-gee-sweet/issues/615))
@@ -350,9 +358,9 @@ Requires `gmail/v1` client and `https://www.googleapis.com/auth/gmail.modify` sc
350
358
  - [ ] Fact-check and word competitive/benchmark claims once real benchmark numbers exist — routed to Amy, triaged out of `backlog` 2026-09-11 ([#585](https://github.com/khuisman/mcp-gee-sweet/issues/585))
351
359
  - [x] QA test-doc cross-reference cleanup after the `drive.md`/`docs.md` split (PR #582) — triaged out of `backlog` 2026-09-11 ([#590](https://github.com/khuisman/mcp-gee-sweet/issues/590)) (PR #851)
352
360
 
353
- **Backlog triage, 2026-09-11 (Kai orchestrator session):** open issues were growing ~2:1 against closures (37 opened vs. 20 closed over the prior 14 days); the 60 issues sitting in unscheduled `backlog` with no version were the other half of that — filed as legitimate findings, never given a real release slot. Retriaged all 60 into actual tiers rather than pruning them: everything fixing/hardening/testing already-shipped functionality went into this v0.9.1 release (the sections above marked "triaged out of `backlog`"), one comments-API cleanup went to v0.9.2 (below, since it overlaps that release's own comments-generalization work), and net-new capabilities that were never started went to Tier 3 (v1.0) or a new lighter-weight tier below Tier 3 (v1.1+) based on scope — see those sections. `backlog` (no version) is now empty; going forward, an issue either has a version or gets triaged promptly, it doesn't accumulate unversioned.
361
+ **Backlog triage, 2026-09-11 (Kai orchestrator session):** open issues were growing ~2:1 against closures (37 opened vs. 20 closed over the prior 14 days); the 60 issues sitting in unscheduled `backlog` with no version were the other half of that — filed as legitimate findings, never given a real release slot. Retriaged all 60 into actual tiers rather than pruning them: everything fixing/hardening/testing already-shipped functionality went into this v0.9.1 release (now v0.9.2) (the sections above marked "triaged out of `backlog`"), one comments-API cleanup went to v0.9.2 (now v0.9.3) (below, since it overlaps that release's own comments-generalization work), and net-new capabilities that were never started went to Tier 3 (v1.0) or a new lighter-weight tier below Tier 3 (v1.1+) based on scope — see those sections. `backlog` (no version) is now empty; going forward, an issue either has a version or gets triaged promptly, it doesn't accumulate unversioned.
354
362
 
355
- ### v0.9.2 — Comments as a first-class, cross-suite capability _(target: [v0.9.2](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.2), before Tier 3 begins)_
363
+ ### v0.9.3 — Comments as a first-class, cross-suite capability _(target: [v0.9.3](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9.3), before Tier 3 begins)_
356
364
 
357
365
  Follows the design in [`docs/decisions/decision-comments-first-class.md`](decisions/decision-comments-first-class.md) ([#661](https://github.com/khuisman/mcp-gee-sweet/issues/661)): the Drive `comments`/`replies` resource is one generic, file-type-agnostic capability, but the codebase only ships a Docs-named `list`/`add`/`resolve` subset (#151). This release generalizes and completes it. Ships a breaking rename — no alias/deprecation mechanism exists in this codebase, so it must be called out explicitly in the release notes. Also carries Joy's code-structure/typing initiative work (see below), slotted here 2026-09-09 to give it release deadlines rather than leaving it open-ended.
358
366
 
@@ -364,18 +372,21 @@ Follows the design in [`docs/decisions/decision-comments-first-class.md`](decisi
364
372
  - [ ] **Decision needed:** deprecate `share_spreadsheet` in favor of `share_file` (a strict superset over the same `permissions()` resource), or keep both and document why. Same pattern as #151/#142, with a longer compatibility tail. ([#665](https://github.com/khuisman/mcp-gee-sweet/issues/665))
365
373
  - [ ] Follow-up cleanup for the docs comments API (`add_doc_comment`/`resolve_doc_comment`/`list_doc_comments`) — same surface #663/#664 generalize, do it there rather than twice; triaged out of `backlog` 2026-09-11 ([#329](https://github.com/khuisman/mcp-gee-sweet/issues/329))
366
374
 
367
- **Drive & Docs follow-ups** _(triaged 2026-09-26; off this release's comments theme, but version-labeled v0.9.2 at filing)_
375
+ **Drive & Docs follow-ups** _(triaged 2026-09-26; off this release's comments theme, but version-labeled v0.9.2 at filing, now v0.9.3)_
368
376
 
369
377
  - [ ] `sync_folder(export_format=...)`: converted Sheets/Slides probably have #814's `modifiedTime` lag and restamp race too. Suspected, not yet observed ([#818](https://github.com/khuisman/mcp-gee-sweet/issues/818))
370
378
  - [ ] Add a tool to set (and read back) Google Docs table column widths — `tableColumnProperties` isn't reachable through any tool today ([#809](https://github.com/khuisman/mcp-gee-sweet/issues/809))
371
379
 
372
- **Code structure & typing (Joy)** _(slotted into v0.9.2 2026-09-09 so Joy's architecture initiative carries release deadlines instead of staying open-ended)_
380
+ **Code structure & typing (Joy)** _(slotted into v0.9.2 (now v0.9.3) 2026-09-09 so Joy's architecture initiative carries release deadlines instead of staying open-ended)_
373
381
 
374
382
  - [ ] Split `tools/sheets/structure.py` by domain (1865 lines, 25 tools) ([#376](https://github.com/khuisman/mcp-gee-sweet/issues/376))
375
383
  - [ ] Plan type-annotation (ANN) adoption — 1635 untyped occurrences; **decision needed** ([#379](https://github.com/khuisman/mcp-gee-sweet/issues/379))
376
384
  - [ ] Decide handling of ASYNC240 findings — sync `pathlib` calls in async tool functions (16 sites); **decision needed** ([#378](https://github.com/khuisman/mcp-gee-sweet/issues/378))
377
385
  - [ ] Investigate other MCP clients' response-overflow handling for platform-specific `MAX_TOOL_RESPONSE_CHARS` guidance ([#555](https://github.com/khuisman/mcp-gee-sweet/issues/555))
378
386
 
387
+ **QA**
388
+ - [ ] Dedicated QA calendar, teardown for creation-only tests, and a `cleanup_stray_artifacts` sweep — split from #304, no second account needed — Aziz ([#886](https://github.com/khuisman/mcp-gee-sweet/issues/886))
389
+
379
390
  ### Tier 3 — Advanced / occasionally needed _(target: [v1.0.0](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av1.0))_
380
391
 
381
392
  **Sheets**
@@ -506,7 +517,7 @@ The test expected the API to silently truncate a 2D array that's wider than the
506
517
  - [x] Google Docs tools (all 13) — `docs.md`
507
518
  - [x] Calendar tools — `calendar.md`
508
519
  - [x] Infrastructure — `infra.md`
509
- - [ ] QA gaps targeted for v0.9 — disposition of v0.8.0 SKIP entries, done, triaged the rest below ([x] [#227](https://github.com/khuisman/mcp-gee-sweet/issues/227), PR #493); dedicated QA Google account + calendar + pollution root-cause fixes, consolidates #225/#226/#249 — deferred out of v0.9.0 (2026-09-02, paid-account cost not justified for the volume); revisit via alternate OAuth client configs ([#304](https://github.com/khuisman/mcp-gee-sweet/issues/304)); second Drive folder + shared drive fixtures, consolidates #44/#48 — done as a full fixture-set re-provision into the `mcp-gee-sweet-shared` Shared Drive (Workspace-org migration overtook the original scope); `TEST_FOLDER_2_ID` half retired, TC-D123 permanently skipped ([x] [#305](https://github.com/khuisman/mcp-gee-sweet/issues/305), PR #677); two Aziz-routed test-doc fixups surfaced by PR #677's fixture re-provisioning, both to be handled during Full Regression pass prep — TC-DOC80 can no longer trip the response cap post-#519, needs a per-case low-cap override ([#678](https://github.com/khuisman/mcp-gee-sweet/issues/678)); several `drive_files.md` cases assume My-Drive semantics now that fixtures live in a Shared Drive (TC-D58/D61/D03/D59/D27/D04/D175), and TC-D27 may expose a real `list_folders` Shared-Drive tool gap ([#680](https://github.com/khuisman/mcp-gee-sweet/issues/680)); domain/public sharing coverage — moved to v0.9.1 (2026-09-02) ([#49](https://github.com/khuisman/mcp-gee-sweet/issues/49)); local-filesystem coverage ([x] [#50](https://github.com/khuisman/mcp-gee-sweet/issues/50), PR #644); environment-constraint coverage — closed 2026-09-02 as duplicate, untangled into #305 (shared drives) / #49 (external + public sharing) / #304 (second account, deferred) / #673 (concurrent-session procedure) ([x] [#53](https://github.com/khuisman/mcp-gee-sweet/issues/53)); concurrent-session test procedure — 2 subagents + `mkdir` barrier against a main-checkout server, split from #53 — procedure + pre-approved-SKIP retirement via PR #675, `aziz.md` release-pass wiring via PR #674; live TC-I24/TC-I02/TC-R36 runs happen in the v0.9.0 Full Regression pass (tracked in `docs/qa/runs/v0.9.0.md`), not a standalone issue ([x] [#673](https://github.com/khuisman/mcp-gee-sweet/issues/673), PRs #674/#675); split `drive.md`/`docs.md` test files by submodule, do before #264/#224 to avoid a file-rename conflict ([x] [#233](https://github.com/khuisman/mcp-gee-sweet/issues/233), PR #582); Playwright tag consistency audit + formalize the required/spot-check/skip tiers, raised while fixing #248, sequenced after #233 ([x] [#264](https://github.com/khuisman/mcp-gee-sweet/issues/264), PR #592, #598); image fixtures for `insert_inline_image`, sequenced after #233 ([x] [#224](https://github.com/khuisman/mcp-gee-sweet/issues/224), PR #639); dedicated screenshots folder, independent of the rest ([x] [#231](https://github.com/khuisman/mcp-gee-sweet/issues/231), PR #583); `add_chart` test cases leave charts behind with no teardown, polluting the shared Sales fixture across every QA run — same pollution-cluster shape as #304/#305 ([x] [#322](https://github.com/khuisman/mcp-gee-sweet/issues/322), PR #633); plus 7 small unit-test-coverage gaps filed 2026-08-02/08, each self-contained/ad hoc pickup — [x] [#487](https://github.com/khuisman/mcp-gee-sweet/issues/487) (PR #559), [x] [#488](https://github.com/khuisman/mcp-gee-sweet/issues/488) (PR #563), [x] [#489](https://github.com/khuisman/mcp-gee-sweet/issues/489) (PR #569), [x] [#490](https://github.com/khuisman/mcp-gee-sweet/issues/490) (PR #572), [x] [#491](https://github.com/khuisman/mcp-gee-sweet/issues/491) (PR #575), [x] [#494](https://github.com/khuisman/mcp-gee-sweet/issues/494) (PR #577), [x] [#495](https://github.com/khuisman/mcp-gee-sweet/issues/495) (PR #634)
520
+ - [ ] QA gaps targeted for v0.9 — disposition of v0.8.0 SKIP entries, done, triaged the rest below ([x] [#227](https://github.com/khuisman/mcp-gee-sweet/issues/227), PR #493); dedicated QA Google account + calendar + pollution root-cause fixes, consolidates #225/#226/#249 — deferred out of v0.9.0 (2026-09-02, paid-account cost not justified for the volume); revisit via alternate OAuth client configs; split 2026-10-04: multi-user testing stays here at v1.0, pollution cleanup moved to [#886](https://github.com/khuisman/mcp-gee-sweet/issues/886) (v0.9.3) ([#304](https://github.com/khuisman/mcp-gee-sweet/issues/304)); second Drive folder + shared drive fixtures, consolidates #44/#48 — done as a full fixture-set re-provision into the `mcp-gee-sweet-shared` Shared Drive (Workspace-org migration overtook the original scope); `TEST_FOLDER_2_ID` half retired, TC-D123 permanently skipped ([x] [#305](https://github.com/khuisman/mcp-gee-sweet/issues/305), PR #677); two Aziz-routed test-doc fixups surfaced by PR #677's fixture re-provisioning, both to be handled during Full Regression pass prep — TC-DOC80 can no longer trip the response cap post-#519, needs a per-case low-cap override ([#678](https://github.com/khuisman/mcp-gee-sweet/issues/678)); several `drive_files.md` cases assume My-Drive semantics now that fixtures live in a Shared Drive (TC-D58/D61/D03/D59/D27/D04/D175), and TC-D27 may expose a real `list_folders` Shared-Drive tool gap ([#680](https://github.com/khuisman/mcp-gee-sweet/issues/680)); domain/public sharing coverage — moved to v0.9.1 (2026-09-02) ([#49](https://github.com/khuisman/mcp-gee-sweet/issues/49)); local-filesystem coverage ([x] [#50](https://github.com/khuisman/mcp-gee-sweet/issues/50), PR #644); environment-constraint coverage — closed 2026-09-02 as duplicate, untangled into #305 (shared drives) / #49 (external + public sharing) / #304 (second account, deferred) / #673 (concurrent-session procedure) ([x] [#53](https://github.com/khuisman/mcp-gee-sweet/issues/53)); concurrent-session test procedure — 2 subagents + `mkdir` barrier against a main-checkout server, split from #53 — procedure + pre-approved-SKIP retirement via PR #675, `aziz.md` release-pass wiring via PR #674; live TC-I24/TC-I02/TC-R36 runs happen in the v0.9.0 Full Regression pass (tracked in `docs/qa/runs/v0.9.0.md`), not a standalone issue ([x] [#673](https://github.com/khuisman/mcp-gee-sweet/issues/673), PRs #674/#675); split `drive.md`/`docs.md` test files by submodule, do before #264/#224 to avoid a file-rename conflict ([x] [#233](https://github.com/khuisman/mcp-gee-sweet/issues/233), PR #582); Playwright tag consistency audit + formalize the required/spot-check/skip tiers, raised while fixing #248, sequenced after #233 ([x] [#264](https://github.com/khuisman/mcp-gee-sweet/issues/264), PR #592, #598); image fixtures for `insert_inline_image`, sequenced after #233 ([x] [#224](https://github.com/khuisman/mcp-gee-sweet/issues/224), PR #639); dedicated screenshots folder, independent of the rest ([x] [#231](https://github.com/khuisman/mcp-gee-sweet/issues/231), PR #583); `add_chart` test cases leave charts behind with no teardown, polluting the shared Sales fixture across every QA run — same pollution-cluster shape as #304/#305 ([x] [#322](https://github.com/khuisman/mcp-gee-sweet/issues/322), PR #633); plus 7 small unit-test-coverage gaps filed 2026-08-02/08, each self-contained/ad hoc pickup — [x] [#487](https://github.com/khuisman/mcp-gee-sweet/issues/487) (PR #559), [x] [#488](https://github.com/khuisman/mcp-gee-sweet/issues/488) (PR #563), [x] [#489](https://github.com/khuisman/mcp-gee-sweet/issues/489) (PR #569), [x] [#490](https://github.com/khuisman/mcp-gee-sweet/issues/490) (PR #572), [x] [#491](https://github.com/khuisman/mcp-gee-sweet/issues/491) (PR #575), [x] [#494](https://github.com/khuisman/mcp-gee-sweet/issues/494) (PR #577), [x] [#495](https://github.com/khuisman/mcp-gee-sweet/issues/495) (PR #634)
510
521
  - See [GitHub Issues (label: qa)](https://github.com/khuisman/mcp-gee-sweet/issues?q=label%3Aqa) for open QA gaps and fixture issues
511
522
 
512
523
  ---
@@ -197,9 +197,9 @@ Requires the `drive.activity.readonly` scope; uses the Drive Activity API v2.
197
197
  | Tool | Description | Key parameters |
198
198
  |---|---|---|
199
199
  | `list_messages` | List messages in the authenticated user's mailbox. | `query?`, `label_ids?`, `max_results?`, `page_token?`, `include_spam_trash?` |
200
- | `get_message` | Fetch a single message by ID, including headers, body, and attachment metadata. | `message_id`, `local_path?` |
200
+ | `get_message` | Fetch a single message by ID, including headers, body, and attachment metadata. | `message_id`, `local_path?`, `include_body?` |
201
201
  | `list_threads` | List conversation threads in the authenticated user's mailbox. | `query?`, `label_ids?`, `max_results?`, `page_token?`, `include_spam_trash?` |
202
- | `get_thread` | Fetch all messages in a conversation thread. | `thread_id`, `local_path?` |
202
+ | `get_thread` | Fetch all messages in a conversation thread. | `thread_id`, `local_path?`, `include_body?` |
203
203
  | `list_labels` | List all labels in the authenticated user's mailbox (system and user-defined). | — |
204
204
  | `send_message` | Send a new email message. | `to`, `subject`, `body`, `cc?`, `bcc?`, `body_html?`, `attachments?` |
205
205
  | `create_draft` | Create a draft email without sending it. | `to`, `subject`, `body`, `cc?`, `bcc?`, `body_html?`, `attachments?` |
@@ -398,6 +398,34 @@ class _PendingMessage:
398
398
  parts: _PayloadParts | None
399
399
 
400
400
 
401
+ # The headers a shaped message surfaces, as (result key, header name). Also the
402
+ # metadataHeaders filter for a body-less fetch, so it requests exactly these (#793).
403
+ _SHAPED_HEADERS = (
404
+ ("from", "From"),
405
+ # Surfaced because reply_to_message prefers it over From when replying to
406
+ # someone else's message (#791).
407
+ ("reply_to", "Reply-To"),
408
+ ("to", "To"),
409
+ ("cc", "Cc"),
410
+ ("bcc", "Bcc"),
411
+ ("subject", "Subject"),
412
+ ("date", "Date"),
413
+ ("message_id", "Message-ID"),
414
+ ("in_reply_to", "In-Reply-To"),
415
+ ("references", "References"),
416
+ )
417
+
418
+
419
+ def _get_format_kwargs(include_body: bool) -> dict[str, Any]:
420
+ """The format kwargs for messages.get / threads.get. 'metadata' returns headers,
421
+ snippet, labels, and size but no payload parts, so no body or attachment data;
422
+ metadataHeaders keeps it from also returning every Received/ARC/DKIM header
423
+ that shaping would drop (#793)."""
424
+ if include_body:
425
+ return {"format": "full"}
426
+ return {"format": "metadata", "metadataHeaders": [name for _, name in _SHAPED_HEADERS]}
427
+
428
+
401
429
  def _start_shaping(msg: dict[str, Any], *, include_body: bool = True) -> _PendingMessage:
402
430
  payload = msg.get("payload") or {}
403
431
  headers = _header_map(payload.get("headers"))
@@ -408,20 +436,7 @@ def _start_shaping(msg: dict[str, Any], *, include_body: bool = True) -> _Pendin
408
436
  "label_ids": msg.get("labelIds") or [],
409
437
  "internal_date": msg.get("internalDate"),
410
438
  "size_estimate": msg.get("sizeEstimate"),
411
- "headers": {
412
- "from": headers.get("from"),
413
- # Surfaced because reply_to_message prefers it over From when replying
414
- # to someone else's message (#791).
415
- "reply_to": headers.get("reply-to"),
416
- "to": headers.get("to"),
417
- "cc": headers.get("cc"),
418
- "bcc": headers.get("bcc"),
419
- "subject": headers.get("subject"),
420
- "date": headers.get("date"),
421
- "message_id": headers.get("message-id"),
422
- "in_reply_to": headers.get("in-reply-to"),
423
- "references": headers.get("references"),
424
- },
439
+ "headers": {key: headers.get(name.lower()) for key, name in _SHAPED_HEADERS},
425
440
  }
426
441
  parts = _extract_bodies_and_attachments(payload) if include_body else None
427
442
  return _PendingMessage(shaped, parts)
@@ -653,7 +668,10 @@ def register(tool):
653
668
 
654
669
  @tool(annotations=ToolAnnotations(title="Get Message", readOnlyHint=True))
655
670
  async def get_message(
656
- message_id: str, local_path: str | None = None, ctx: Context = None
671
+ message_id: str,
672
+ local_path: str | None = None,
673
+ include_body: bool = True,
674
+ ctx: Context = None,
657
675
  ) -> dict[str, Any]:
658
676
  """
659
677
  Fetch a single message by ID, including headers, body, and attachment metadata.
@@ -663,6 +681,9 @@ def register(tool):
663
681
  local_path: Optional local filesystem path (file or directory) to write the
664
682
  message JSON to instead of returning it inline. Bypasses the
665
683
  response-size cap, for messages with very large bodies.
684
+ include_body: When False, fetch only metadata (Gmail's format='metadata'):
685
+ no body is downloaded, and body_plain, body_html, and
686
+ attachments are left out of the result.
666
687
 
667
688
  Returns:
668
689
  Message with id, thread_id, snippet, label_ids, headers (from/to/cc/bcc/
@@ -678,8 +699,9 @@ def register(tool):
678
699
  set; a separately delivered body already over the cap raises before
679
700
  being downloaded. If local_path is set, returns {local_path, message_id,
680
701
  bytes_written} instead, plus body_fetch_errors / body_decode_errors
681
- (each entry with message_id) when present. On API failure,
682
- {"error": "..."}.
702
+ (each entry with message_id) when present. With include_body=False, only
703
+ id, thread_id, snippet, label_ids, internal_date, size_estimate, and
704
+ headers. On API failure, {"error": "..."}.
683
705
  """
684
706
  if unauthorized := get_gmail_unauthorized_message():
685
707
  return {"error": unauthorized}
@@ -688,15 +710,16 @@ def register(tool):
688
710
  msg = await execute_in_thread(
689
711
  lc.gmail_service.users()
690
712
  .messages()
691
- .get(userId=_USER, id=message_id, format="full")
713
+ .get(userId=_USER, id=message_id, **_get_format_kwargs(include_body))
692
714
  .execute,
693
715
  lc.gmail_service,
694
716
  )
695
717
  except Exception as e:
696
718
  return {"error": str(e)}
697
719
 
698
- hint = "The message body is large. "
699
- pending = [_start_shaping(msg, include_body=True)]
720
+ # No body is fetched with include_body=False, so only headers can be large.
721
+ hint = "The message body is large. " if include_body else "The message headers are large. "
722
+ pending = [_start_shaping(msg, include_body=include_body)]
700
723
  if not local_path:
701
724
  enforce_response_size_floor(
702
725
  _deferred_body_bytes(pending), tool_name="get_message", hint=hint
@@ -784,7 +807,10 @@ def register(tool):
784
807
 
785
808
  @tool(annotations=ToolAnnotations(title="Get Thread", readOnlyHint=True))
786
809
  async def get_thread(
787
- thread_id: str, local_path: str | None = None, ctx: Context = None
810
+ thread_id: str,
811
+ local_path: str | None = None,
812
+ include_body: bool = True,
813
+ ctx: Context = None,
788
814
  ) -> dict[str, Any]:
789
815
  """
790
816
  Fetch all messages in a conversation thread.
@@ -794,9 +820,14 @@ def register(tool):
794
820
  local_path: Optional local filesystem path (file or directory) to write the
795
821
  thread JSON to instead of returning it inline. Bypasses the
796
822
  response-size cap, for threads with very large bodies.
823
+ include_body: When False, fetch only each message's metadata (Gmail's
824
+ format='metadata'): no body is downloaded, and body_plain,
825
+ body_html, and attachments are left out. Use it to list a
826
+ thread too large to read whole, then get_message each ID.
797
827
 
798
828
  Returns:
799
- Thread with id, snippet, history_id, and messages (same shape as get_message).
829
+ Thread with id, snippet, history_id, and messages (same shape as get_message,
830
+ including its include_body=False shape).
800
831
  Raises an error naming the size if the thread is over the response-size cap
801
832
  and local_path is not set. If local_path is set, returns {local_path,
802
833
  thread_id, message_count, bytes_written} instead, plus body_fetch_errors /
@@ -810,15 +841,20 @@ def register(tool):
810
841
  thread = await execute_in_thread(
811
842
  lc.gmail_service.users()
812
843
  .threads()
813
- .get(userId=_USER, id=thread_id, format="full")
844
+ .get(userId=_USER, id=thread_id, **_get_format_kwargs(include_body))
814
845
  .execute,
815
846
  lc.gmail_service,
816
847
  )
817
848
  except Exception as e:
818
849
  return {"error": str(e)}
819
850
 
820
- hint = "The thread is large; use get_message on individual IDs. "
821
- pending = [_start_shaping(m, include_body=True) for m in thread.get("messages", [])]
851
+ hint = (
852
+ "The thread is large; call get_thread with include_body=False to list its "
853
+ "message IDs and headers, then get_message on individual IDs. "
854
+ if include_body
855
+ else "The thread has too many messages to list inline. "
856
+ )
857
+ pending = [_start_shaping(m, include_body=include_body) for m in thread.get("messages", [])]
822
858
  if not local_path:
823
859
  enforce_response_size_floor(
824
860
  _deferred_body_bytes(pending), tool_name="get_thread", hint=hint