mcp-gee-sweet 0.9.2.dev212__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.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/PKG-INFO +1 -1
  2. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/gmail-test-plan.md +3 -2
  3. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/gmail.md +30 -5
  4. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/roadmap.md +9 -1
  5. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/tools.md +2 -2
  6. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/gmail.py +61 -25
  7. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_gmail.py +128 -1
  8. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/cleanup-worktrees.md +0 -0
  9. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/merge-pr.md +0 -0
  10. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/notify-kai.md +0 -0
  11. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/notify-partner.md +0 -0
  12. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/orchestrator.md +0 -0
  13. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/prep-for-pr.md +0 -0
  14. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/qa-kickoff.md +0 -0
  15. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/release.md +0 -0
  16. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/retro.md +0 -0
  17. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/roadmap-status.md +0 -0
  18. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/start-worktree.md +0 -0
  19. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/team-member.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/commands/verify-pr.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/settings.json +0 -0
  22. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/amy.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/aziz.md +0 -0
  24. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/bob.md +0 -0
  25. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/dev.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/joy.md +0 -0
  27. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/kai.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.claude/team-roles/qa.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.dockerignore +0 -0
  30. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/dependabot.yml +0 -0
  34. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/ci.yml +0 -0
  35. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/docs.yml +0 -0
  36. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/publish-dev.yml +0 -0
  37. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.github/workflows/release.yml +0 -0
  38. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.gitignore +0 -0
  39. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.pre-commit-config.yaml +0 -0
  40. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/.python-version +0 -0
  41. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/CHANGELOG.md +0 -0
  42. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/CLAUDE.md +0 -0
  43. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/CODE_OF_CONDUCT.md +0 -0
  44. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/CONTRIBUTING.md +0 -0
  45. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/Dockerfile +0 -0
  46. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/LICENSE +0 -0
  47. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/Makefile +0 -0
  48. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/README.md +0 -0
  49. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/SECURITY.md +0 -0
  50. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docker-compose.yml +0 -0
  51. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/assets/logo.png +0 -0
  52. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/auth.md +0 -0
  53. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/client-setup.md +0 -0
  54. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/configuration.md +0 -0
  55. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-async-tool-execution.md +0 -0
  56. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  57. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-cache-invalidation.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-chart-theming.md +0 -0
  59. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-comments-first-class.md +0 -0
  60. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-composite-tools.md +0 -0
  61. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-dev-team.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-docs-formatting.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-fork.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-gmail-domain.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  70. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  71. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-prompt-qa-role.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-publishing.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-release-docs-roles.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-repositioning.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  77. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  78. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  79. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/decision-testing.md +0 -0
  80. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/decisions/index.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/blockquote-representation.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/borderless-table-columns.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/doc-to-markdown.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/docs-ast-pipeline.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/effectiveformat-spike.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/heading-anchor-resolution.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/image-conversion.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/index.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/markdown-support.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/module-history.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/phase3-theme-system.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design/sse-consent-off-event-loop.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/design.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/index.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/known-limitations.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/notes-read.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/README.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  99. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  101. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  102. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  103. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  104. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  105. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  106. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  107. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  108. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  109. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  110. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  111. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  112. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  113. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  114. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  115. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  116. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/fixtures.template.md +0 -0
  117. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/operations.yaml +0 -0
  118. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/playwright_oauth.md +0 -0
  119. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/.gitkeep +0 -0
  120. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-02.md +0 -0
  121. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-14.md +0 -0
  122. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-17.md +0 -0
  123. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-18.md +0 -0
  124. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  125. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  126. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-06-28.md +0 -0
  127. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-docs.md +0 -0
  128. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  129. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-infra.md +0 -0
  130. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  131. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-07-04.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/results/2026-09-04.md +0 -0
  133. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/retro-v0.8.0.md +0 -0
  134. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/retro-v0.9.0.md +0 -0
  135. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/run.md +0 -0
  136. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/README.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/v0.8.0.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/v0.8.1.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/runs/v0.9.0.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/setup.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/calendar.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_content.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_layout.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_style.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/docs_tables.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_activity.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_files.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_sharing.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/drive_transfer.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/infra.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_charts.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_mgmt.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_read.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/qa/tests/sheets_write.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/docs/style-guide.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/mkdocs.yml +0 -0
  157. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/pyproject.toml +0 -0
  158. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/pyrightconfig.json +0 -0
  159. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/check_supports_all_drives.py +0 -0
  160. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/gen_tool_docs.py +0 -0
  161. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/lane_context_hook.py +0 -0
  162. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/oauth_setup.py +0 -0
  163. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/qa_gmail_fixtures.py +0 -0
  164. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/setup_team.sh +0 -0
  165. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/scripts/write_team_mcp_configs.sh +0 -0
  166. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/server.json +0 -0
  167. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/.env.template +0 -0
  168. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/__init__.py +0 -0
  169. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/auth.py +0 -0
  170. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/cache.py +0 -0
  171. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/http_transport.py +0 -0
  172. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/server.py +0 -0
  173. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  174. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/cache.py +0 -0
  175. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  176. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  177. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  178. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  179. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  180. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
  181. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  182. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  190. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  191. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  193. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  194. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  197. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  198. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  199. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/__init__.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/conftest.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/__init__.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_activity.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_files.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_sharing.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/drive/test_transfer.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/integration/__init__.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/integration/conftest.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/integration/test_local_fs_harness.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/__init__.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/test_data.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/test_helpers.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/sheets/test_structure.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_auth.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_cache.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_calendar.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_check_supports_all_drives.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_concurrency.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_anchors.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_comments.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_content.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_core.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_editing.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_images.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_indices.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_layout.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_markdown_export.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_named_ranges.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_style.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_docs_tables.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_gen_tool_docs.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_lane_context_hook.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_qa_gmail_fixtures.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_response_limits.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_server.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_server_json.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/tests/test_team_mcp_configs.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev212 → mcp_gee_sweet-0.9.2.dev215}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gee-sweet
3
- Version: 0.9.2.dev212
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
 
@@ -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
 
@@ -196,6 +196,8 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
196
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))
197
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))
198
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))
199
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))
200
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)
201
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))
@@ -242,6 +244,8 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
242
244
  - [ ] `ast_to_markdown.py`: image-render duplication + falsy-based code-block buffer flush ([#600](https://github.com/khuisman/mcp-gee-sweet/issues/600))
243
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))
244
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))
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))
245
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)
246
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))
247
251
  - [ ] `TestIsolatedDepthRunGlyph` asserts request shape, not rendered nesting outcome (PR #711) ([#715](https://github.com/khuisman/mcp-gee-sweet/issues/715))
@@ -270,6 +274,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
270
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)
271
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))
272
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))
273
278
 
274
279
  **Calendar hardening** _(triaged out of `backlog` 2026-09-11)_
275
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))
@@ -379,6 +384,9 @@ Follows the design in [`docs/decisions/decision-comments-first-class.md`](decisi
379
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))
380
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))
381
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
+
382
390
  ### Tier 3 — Advanced / occasionally needed _(target: [v1.0.0](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av1.0))_
383
391
 
384
392
  **Sheets**
@@ -509,7 +517,7 @@ The test expected the API to silently truncate a 2D array that's wider than the
509
517
  - [x] Google Docs tools (all 13) — `docs.md`
510
518
  - [x] Calendar tools — `calendar.md`
511
519
  - [x] Infrastructure — `infra.md`
512
- - [ ] 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)
513
521
  - See [GitHub Issues (label: qa)](https://github.com/khuisman/mcp-gee-sweet/issues?q=label%3Aqa) for open QA gaps and fixture issues
514
522
 
515
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
@@ -49,6 +49,22 @@ def _raw_payload_from_send_call(gmail_svc) -> bytes:
49
49
  _gmail_tool, _gmail_tools = _make_tool_registry()
50
50
  gmail_module.register(_gmail_tool)
51
51
 
52
+ # Every header a shaped message surfaces, which a body-less fetch requests via
53
+ # metadataHeaders (#793). Spelled out rather than imported from gmail.py, so a
54
+ # change to the filter has to be made here too.
55
+ _SHAPED_HEADER_NAMES = [
56
+ "From",
57
+ "Reply-To",
58
+ "To",
59
+ "Cc",
60
+ "Bcc",
61
+ "Subject",
62
+ "Date",
63
+ "Message-ID",
64
+ "In-Reply-To",
65
+ "References",
66
+ ]
67
+
52
68
 
53
69
  class TestGmailNotAuthorized:
54
70
  """#790 / PR #807 QA round 1: when the OAuth token lacks only the Gmail scope,
@@ -189,6 +205,42 @@ class TestGetMessage:
189
205
 
190
206
  assert "error" in result
191
207
 
208
+ async def test_include_body_false_fetches_metadata_only(self):
209
+ """#793: metadata format has no payload parts, so no body keys and no fetches."""
210
+ gmail_svc = MagicMock()
211
+ messages = gmail_svc.users.return_value.messages.return_value
212
+ messages.get.return_value.execute.return_value = {
213
+ "id": "m1",
214
+ "threadId": "t1",
215
+ "snippet": "Hello",
216
+ "labelIds": ["INBOX"],
217
+ "internalDate": "1700000000000",
218
+ "sizeEstimate": 162270,
219
+ "payload": {
220
+ "mimeType": "multipart/mixed",
221
+ "headers": [
222
+ {"name": "From", "value": "a@example.com"},
223
+ {"name": "Subject", "value": "Hi"},
224
+ ],
225
+ },
226
+ }
227
+ ctx = _make_ctx(gmail_service=gmail_svc)
228
+
229
+ result = await _gmail_tools["get_message"](message_id="m1", include_body=False, ctx=ctx)
230
+
231
+ messages.get.assert_called_once_with(
232
+ userId="me", id="m1", format="metadata", metadataHeaders=_SHAPED_HEADER_NAMES
233
+ )
234
+ assert result["id"] == "m1"
235
+ assert result["thread_id"] == "t1"
236
+ assert result["snippet"] == "Hello"
237
+ assert result["size_estimate"] == 162270
238
+ assert result["headers"]["from"] == "a@example.com"
239
+ assert result["headers"]["subject"] == "Hi"
240
+ for key in ("body_plain", "body_html", "attachments"):
241
+ assert key not in result
242
+ messages.attachments.return_value.get.assert_not_called()
243
+
192
244
 
193
245
  def _b64(raw: bytes, *, pad: bool = True) -> str:
194
246
  encoded = base64.urlsafe_b64encode(raw).decode()
@@ -837,9 +889,40 @@ class TestMessageSizeCap:
837
889
  }
838
890
  ctx = _make_ctx(gmail_service=gmail_svc)
839
891
 
840
- with pytest.raises(ValueError, match=r"get_thread.*Pass local_path"):
892
+ with pytest.raises(ValueError, match=r"get_thread.*include_body=False.*Pass local_path"):
841
893
  await _gmail_tools["get_thread"](thread_id="t1", ctx=ctx)
842
894
 
895
+ async def test_get_message_body_less_over_cap_blames_headers_not_body(self, small_cap):
896
+ """#793 QA: no body is fetched with include_body=False, so the hint can't
897
+ call the body large."""
898
+ gmail_svc = MagicMock()
899
+ gmail_svc.users.return_value.messages.return_value.get.return_value.execute.return_value = {
900
+ "id": "m1",
901
+ "threadId": "t1",
902
+ "payload": {"headers": [{"name": "References", "value": "<r@x> " * 100}]},
903
+ }
904
+ ctx = _make_ctx(gmail_service=gmail_svc)
905
+
906
+ with pytest.raises(ValueError, match=r"get_message.*headers are large") as exc:
907
+ await _gmail_tools["get_message"](message_id="m1", include_body=False, ctx=ctx)
908
+ assert "body" not in str(exc.value)
909
+
910
+ async def test_get_thread_body_less_over_cap_drops_include_body_hint(self, small_cap):
911
+ """#793: suggesting include_body=False to a call already using it is useless."""
912
+ gmail_svc = MagicMock()
913
+ gmail_svc.users.return_value.threads.return_value.get.return_value.execute.return_value = {
914
+ "id": "t1",
915
+ "messages": [
916
+ {"id": f"m{i}", "threadId": "t1", "snippet": "s" * 50, "payload": {}}
917
+ for i in range(5)
918
+ ],
919
+ }
920
+ ctx = _make_ctx(gmail_service=gmail_svc)
921
+
922
+ with pytest.raises(ValueError, match=r"get_thread.*Pass local_path") as exc:
923
+ await _gmail_tools["get_thread"](thread_id="t1", include_body=False, ctx=ctx)
924
+ assert "include_body" not in str(exc.value)
925
+
843
926
  async def test_get_thread_local_path_writes_full_thread(self, small_cap, tmp_path):
844
927
  gmail_svc = self._large()
845
928
  gmail_svc.users.return_value.threads.return_value.get.return_value.execute.return_value = {
@@ -970,6 +1053,50 @@ class TestGetThread:
970
1053
 
971
1054
  assert "error" in result
972
1055
 
1056
+ async def test_default_fetches_full_format(self):
1057
+ gmail_svc = MagicMock()
1058
+ threads = gmail_svc.users.return_value.threads.return_value
1059
+ threads.get.return_value.execute.return_value = {"id": "t1", "messages": []}
1060
+ ctx = _make_ctx(gmail_service=gmail_svc)
1061
+
1062
+ await _gmail_tools["get_thread"](thread_id="t1", ctx=ctx)
1063
+
1064
+ threads.get.assert_called_once_with(userId="me", id="t1", format="full")
1065
+
1066
+ async def test_include_body_false_lists_ids_and_headers_only(self):
1067
+ """#793: a thread too large to read whole can still be listed by message ID."""
1068
+ gmail_svc = MagicMock()
1069
+ threads = gmail_svc.users.return_value.threads.return_value
1070
+ threads.get.return_value.execute.return_value = {
1071
+ "id": "t1",
1072
+ "historyId": "5",
1073
+ "messages": [
1074
+ {
1075
+ "id": f"m{i}",
1076
+ "threadId": "t1",
1077
+ "snippet": f"snippet {i}",
1078
+ "payload": {
1079
+ "mimeType": "multipart/mixed",
1080
+ "headers": [{"name": "Subject", "value": f"Re: {i}"}],
1081
+ },
1082
+ }
1083
+ for i in (1, 2)
1084
+ ],
1085
+ }
1086
+ ctx = _make_ctx(gmail_service=gmail_svc)
1087
+
1088
+ result = await _gmail_tools["get_thread"](thread_id="t1", include_body=False, ctx=ctx)
1089
+
1090
+ threads.get.assert_called_once_with(
1091
+ userId="me", id="t1", format="metadata", metadataHeaders=_SHAPED_HEADER_NAMES
1092
+ )
1093
+ assert [m["id"] for m in result["messages"]] == ["m1", "m2"]
1094
+ assert result["messages"][1]["headers"]["subject"] == "Re: 2"
1095
+ for message in result["messages"]:
1096
+ for key in ("body_plain", "body_html", "attachments"):
1097
+ assert key not in message
1098
+ gmail_svc.users.return_value.messages.return_value.attachments.assert_not_called()
1099
+
973
1100
 
974
1101
  class TestListLabels:
975
1102
  async def test_maps_system_and_user_labels(self):