mcp-gee-sweet 0.9.2.dev233__tar.gz → 0.9.2.dev236__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 (247) hide show
  1. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/PKG-INFO +1 -1
  2. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/sse-consent-off-event-loop.md +1 -1
  3. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/infra.md +31 -1
  4. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/roadmap.md +1 -1
  5. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/server.py +17 -2
  6. mcp_gee_sweet-0.9.2.dev236/src/mcp_gee_sweet/sse_shutdown.py +60 -0
  7. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/drive/files.py +5 -7
  8. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_server.py +26 -1
  9. mcp_gee_sweet-0.9.2.dev236/tests/test_sse_shutdown.py +189 -0
  10. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/cleanup-worktrees.md +0 -0
  11. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/merge-pr.md +0 -0
  12. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/notify-kai.md +0 -0
  13. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/notify-partner.md +0 -0
  14. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/orchestrator.md +0 -0
  15. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/prep-for-pr.md +0 -0
  16. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/qa-kickoff.md +0 -0
  17. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/release.md +0 -0
  18. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/retro.md +0 -0
  19. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/roadmap-status.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/start-worktree.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/team-member.md +0 -0
  22. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/commands/verify-pr.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/settings.json +0 -0
  24. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/amy.md +0 -0
  25. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/aziz.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/bob.md +0 -0
  27. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/dev.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/joy.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/kai.md +0 -0
  30. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.claude/team-roles/qa.md +0 -0
  31. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.dockerignore +0 -0
  32. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  33. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  34. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  35. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/dependabot.yml +0 -0
  36. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/workflows/ci.yml +0 -0
  37. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/workflows/docs.yml +0 -0
  38. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/workflows/publish-dev.yml +0 -0
  39. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.github/workflows/release.yml +0 -0
  40. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.gitignore +0 -0
  41. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.pre-commit-config.yaml +0 -0
  42. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/.python-version +0 -0
  43. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/CHANGELOG.md +0 -0
  44. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/CLAUDE.md +0 -0
  45. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/CODE_OF_CONDUCT.md +0 -0
  46. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/CONTRIBUTING.md +0 -0
  47. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/Dockerfile +0 -0
  48. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/LICENSE +0 -0
  49. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/Makefile +0 -0
  50. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/README.md +0 -0
  51. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/SECURITY.md +0 -0
  52. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docker-compose.yml +0 -0
  53. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/assets/logo.png +0 -0
  54. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/auth.md +0 -0
  55. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/client-setup.md +0 -0
  56. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/configuration.md +0 -0
  57. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-async-tool-execution.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  59. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-cache-invalidation.md +0 -0
  60. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-chart-theming.md +0 -0
  61. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-comments-first-class.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-composite-tools.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-dev-team.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-docs-formatting.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-fork.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-gmail-domain.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  70. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  71. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-prompt-qa-role.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-publishing.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-release-docs-roles.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-repositioning.md +0 -0
  77. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  78. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  79. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  80. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-testing.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/decision-tool-error-visibility.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/decisions/index.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/blockquote-representation.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/borderless-table-columns.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/doc-to-markdown.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/docs-ast-pipeline.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/effectiveformat-spike.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/heading-anchor-resolution.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/image-conversion.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/index.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/markdown-support.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/module-history.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design/phase3-theme-system.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/design.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/index.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/known-limitations.md +0 -0
  99. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/notes-read.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/README.md +0 -0
  101. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  102. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  103. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  104. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  105. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  106. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  107. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  108. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  109. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  110. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  111. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  112. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  113. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  114. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  115. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  116. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  117. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  118. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  119. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/fixtures.template.md +0 -0
  120. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/gmail-test-plan.md +0 -0
  121. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/operations.yaml +0 -0
  122. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/playwright_oauth.md +0 -0
  123. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/.gitkeep +0 -0
  124. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-02.md +0 -0
  125. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-14.md +0 -0
  126. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-17.md +0 -0
  127. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-18.md +0 -0
  128. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  129. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  130. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-06-28.md +0 -0
  131. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-07-04-docs.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  133. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-07-04-infra.md +0 -0
  134. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  135. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-07-04.md +0 -0
  136. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/results/2026-09-04.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/retro-v0.8.0.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/retro-v0.9.0.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/run.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/runs/README.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/runs/v0.8.0.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/runs/v0.8.1.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/runs/v0.9.0.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/setup.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/calendar.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/docs_content.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/docs_layout.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/docs_style.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/docs_tables.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/drive_activity.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/drive_files.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/drive_sharing.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/drive_transfer.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/gmail.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/sheets_charts.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/sheets_mgmt.md +0 -0
  157. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/sheets_read.md +0 -0
  158. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/qa/tests/sheets_write.md +0 -0
  159. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/style-guide.md +0 -0
  160. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/docs/tools.md +0 -0
  161. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/mkdocs.yml +0 -0
  162. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/pyproject.toml +0 -0
  163. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/pyrightconfig.json +0 -0
  164. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/check_supports_all_drives.py +0 -0
  165. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/gen_tool_docs.py +0 -0
  166. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/lane_context_hook.py +0 -0
  167. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/oauth_setup.py +0 -0
  168. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/qa_cancel_insert_local_images.py +0 -0
  169. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/qa_gmail_fixtures.py +0 -0
  170. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/setup_team.sh +0 -0
  171. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/scripts/write_team_mcp_configs.sh +0 -0
  172. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/server.json +0 -0
  173. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/.env.template +0 -0
  174. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/__init__.py +0 -0
  175. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/auth.py +0 -0
  176. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/cache.py +0 -0
  177. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/http_transport.py +0 -0
  178. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  179. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/cache.py +0 -0
  180. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  181. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  182. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  190. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  191. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  193. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  194. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  197. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  198. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  199. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/__init__.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/conftest.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/drive/__init__.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/drive/test_activity.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/drive/test_files.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/drive/test_sharing.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/drive/test_transfer.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/integration/__init__.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/integration/conftest.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/integration/test_local_fs_harness.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/sheets/__init__.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/sheets/test_data.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/sheets/test_helpers.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/sheets/test_structure.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_auth.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_cache.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_calendar.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_check_supports_all_drives.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_concurrency.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_anchors.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_comments.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_content.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_core.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_editing.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_images.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_indices.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_layout.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_markdown_export.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_named_ranges.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_style.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_docs_tables.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_gen_tool_docs.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_gmail.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_lane_context_hook.py +0 -0
  243. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_qa_gmail_fixtures.py +0 -0
  244. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_response_limits.py +0 -0
  245. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_server_json.py +0 -0
  246. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/tests/test_team_mcp_configs.py +0 -0
  247. {mcp_gee_sweet-0.9.2.dev233 → mcp_gee_sweet-0.9.2.dev236}/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.dev233
3
+ Version: 0.9.2.dev236
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
@@ -60,7 +60,7 @@ Patching mcp's transport instead was rejected: it means carrying a copy of `conn
60
60
  | Stray callback request | | degrades in 0.08s (`MismatchingStateError`) |
61
61
  | Missing-scope token, client POSTs `initialize` on the endpoint event, then SIGTERM | n/a (no endpoint before the raise) | before §5: 25 of 25 hang. After: 0 of 25, POST `202`; a real `sse_client` initializes and gets the missing-scopes message as the tool error |
62
62
 
63
- An `Exception in ASGI application ... Expected ASGI message 'http.response.body'` traceback at shutdown appears on `develop` too, for any SSE stream open at SIGTERM, so it's unrelated to this change.
63
+ An `Exception in ASGI application ... Expected ASGI message 'http.response.body'` traceback at shutdown appeared on `develop` too, for any SSE stream open at SIGTERM, so it was unrelated to this change. #868 fixed it with `SingleResponseGuard` (`sse_shutdown.py`).
64
64
 
65
65
  ## Not changed
66
66
 
@@ -1026,7 +1026,7 @@ Start `uv run mcp-gee-sweet --transport sse` with `AUTH_METHOD=oauth`, `TOKEN_PA
1026
1026
  - 5: the stderr file has `Starting without Google access: ... the server shut down before the browser consent completed`
1027
1027
  - 5: `lsof -i :<port>` and `lsof -i :<cb>` are both empty afterward
1028
1028
 
1029
- An `Exception in ASGI application ... Expected ASGI message 'http.response.body', but got 'http.response.start'` traceback during shutdown isn't a failure of this case: any SSE stream open at SIGTERM produces it, with or without a consent wait (pre-existing, seen on `develop` before #833).
1029
+ An `Exception in ASGI application ... Expected ASGI message 'http.response.body', but got 'http.response.start'` traceback during shutdown isn't a failure of this case. Any SSE stream open at SIGTERM used to produce it, with or without a consent wait (pre-existing, seen on `develop` before #833). Fixed in #868, which TC-I47 covers.
1030
1030
 
1031
1031
  **Cleanup:** if the server is still running, SIGKILL its process group and record the case as failed.
1032
1032
 
@@ -1128,3 +1128,33 @@ Live `mcp-gee-sweet-sky` server, OAuth, mcp 2.3.0. 1: `Error executing tool list
1128
1128
 
1129
1129
  **Result (2026-10-04, PR #905 round 2 @ 42ab5e6, Sky) ✅ PASS**
1130
1130
  Live `mcp-gee-sweet-sky` server, mcp 2.3.0: steps 1 and 2 return the same `HttpError 404 ... Requested entity was not found.` text as round 1. Extra check over a stdio subprocess with the same credentials (`DEBUG_LEVEL=INFO`): the server logs `WARNING mcp_gee_sweet.server Reporting HttpError to the client: ...` with the full traceback through `execute_in_thread`, and the access line is `"TOOL list_sheets" 500`.
1131
+
1132
+ ---
1133
+
1134
+ ### TC-I47: SIGTERM with an SSE stream open shuts down without an ASGI traceback (issue #868) ⚠️ local-filesystem
1135
+
1136
+ **Background:** on SIGTERM, sse_starlette cancels an open SSE stream without sending its closing chunk, and mcp's `/sse` endpoint then returns an empty `Response()` on the same connection. uvicorn rejected that second response with `Exception in ASGI application ... RuntimeError: Expected ASGI message 'http.response.body', but got 'http.response.start'` on every shutdown with a stream open, and the client saw a truncated chunked stream. `SingleResponseGuard` (`sse_shutdown.py`) now closes the cut-off stream and drops the late second response, but only once `AppStatus.should_exit` is set. Before shutdown it passes everything through, so uvicorn still reports a genuine double response, and after an ordinary client disconnect uvicorn no-ops mcp's late `Response()` itself. `main()` serves the guarded app for plain `--transport sse` as well as `--reload`, since `mcp.run(transport="sse")` builds its own unguarded app. It launches it the way mcp's `run_sse_async()` does (`anyio.run(uvicorn.Server(config).serve)`), not with `uvicorn.run()`, which would pick uvloop when installed.
1137
+
1138
+ **Setup:** none beyond a checkout of this branch. No Google credentials are needed: the case only needs an idle, fully started SSE connection.
1139
+
1140
+ **Action**
1141
+ Start `uv run mcp-gee-sweet --transport sse` with `HOST=127.0.0.1`, `PORT=<free port>`, `AUTH_METHOD=oauth`, `CREDENTIALS_PATH=<nonexistent>`, `TOKEN_PATH=<nonexistent>`, `DEBUG_LEVEL=DEBUG`, `PYTHONUNBUFFERED=1`, stdout and stderr to one file.
1142
+ 1. Open `GET http://127.0.0.1:<port>/sse` (e.g. Python `urllib.request.urlopen`) and read the first line (`event: endpoint`). Wait 3 seconds
1143
+ 2. Send the server process `SIGTERM` (not SIGKILL) and time its exit. Then read the rest of the SSE response to EOF
1144
+ 3. Repeat steps 1–2 two more times against a fresh server, then once more with `--reload` added to the command line
1145
+ 4. Start a fresh server the same way; with the `mcp` SDK's `sse_client` + `ClientSession`, `initialize` and `list_tools`, close the session, then SIGTERM the server
1146
+
1147
+ **Checks**
1148
+ - 2, 3: the log has no `Exception in ASGI application`, no `RuntimeError: Expected ASGI message`, and no `ASGI callable returned without completing response`. (The setup's nonexistent `CREDENTIALS_PATH` logs a `DEBUG ... Auth failure detail` traceback ending in `RuntimeError: '<path>' not found` at connection time; that's the expected degraded start, not a failure.) With `DEBUG_LEVEL=DEBUG` it has `DEBUG mcp_gee_sweet.sse_shutdown Dropping a second response on /sse during shutdown (#868)`
1149
+ - 2, 3: the server exits within a few seconds of SIGTERM, and `lsof -i :<port>` is empty afterwards
1150
+ - 2, 3: reading the SSE response to the end ends with a clean EOF, not an `IncompleteRead` (before this fix the chunked stream was never terminated)
1151
+ - 4: `initialize` and `list_tools` succeed (the guard doesn't touch a normal session), and the log has no traceback other than the expected `Auth failure detail` one
1152
+ - 4: the log has no `Dropping a second response` line at all. The session's stream closed before SIGTERM, so the guard has nothing to drop, and it acts only during shutdown (round 1 found it acting on an ordinary client disconnect). Don't check the line's order against uvicorn's `Shutting down` instead: in steps 2–3 the two are logged in either order during a real shutdown.
1153
+
1154
+ **Cleanup:** SIGKILL any server still running (and record the case as failed).
1155
+
1156
+ **Result (2026-10-05, PR #911 round 1 @ d6ba1ec, Sky) ✅ PASS**
1157
+ uvicorn 0.47.0, run through `uv run` (so the parent's exit code is uv's 143 after forwarding SIGTERM; `--reload` exits 0). Steps 2–3 (three plain runs plus one `--reload`): every log has the `Dropping a second response on /sse (#868)` line and none of the three error strings. The server exits 0.2–0.8s after SIGTERM, the SSE response reads to a clean EOF, and the port is free afterwards. Baseline: the same driver against an `origin/develop` export logs `Exception in ASGI application ... RuntimeError: Expected ASGI message 'http.response.body', but got 'http.response.start'`, and the client gets `IncompleteRead(64 bytes read)`. Step 4: `initialize` and `list_tools` (139 tools) succeed, with no ASGI error. But the log shows `Dropping a second response on /sse (#868)` right after the client closed its session, about 1s *before* SIGTERM. So the guard also handles mcp's late `Response()` after an ordinary client disconnect, not only at shutdown (see the round-1 PR comment). Side check: `--transport streamable-http` with an open `GET /mcp` stream still logs `ASGI callable returned without completing response.` on SIGTERM, and the client gets `IncompleteRead(0 bytes read)`. That transport isn't in this PR's scope; filed as #917.
1158
+
1159
+ **Result (2026-10-05, PR #911 round 2 @ 1f138a5, Sky) ✅ PASS**
1160
+ Same drivers as round 1. Steps 2–3 (three plain runs plus one `--reload`): each log has `Dropping a second response on /sse during shutdown (#868)` and none of the three error strings. The server exits 0.2–0.6s after SIGTERM, every SSE response reads to a clean EOF, and the ports are free. The drop line and uvicorn's `Shutting down` were logged in either order across these runs, so I changed step 4's check from an ordering check to an absence check. Step 4: `initialize` and `list_tools` (139 tools) succeed, and the log has no `Dropping` line and no ASGI error. So after an ordinary client disconnect, the late `Response()` now reaches uvicorn, which ignores it without error. Plain `--transport sse` now launches via `anyio.run(uvicorn.Server(config).serve)` and behaves the same as `--reload`.
@@ -330,7 +330,7 @@ Requires `gmail/v1` client and `https://www.googleapis.com/auth/gmail.modify` sc
330
330
  - [ ] TC-DOC80 can no longer trip the response-size cap post-#519 — large-doc fixture is under the raised default; needs a per-case low-cap override — Aziz ([#678](https://github.com/khuisman/mcp-gee-sweet/issues/678))
331
331
  - [ ] Define the correctness rubric and own execution for the composite-vs-primitive efficiency benchmark — Aziz ([#584](https://github.com/khuisman/mcp-gee-sweet/issues/584))
332
332
  - [ ] QA `.env` has no single source of truth — `setup_team.sh` only copies `.env` into a worktree when absent, so fixture-ID changes never propagate (had to hand-sync 9 copies for #305); also `run.md` vs `setup.md` disagree on `docs/qa/.env` vs repo-root `.env`. Surfaced by PR #677, relates to [#648](https://github.com/khuisman/mcp-gee-sweet/issues/648) ([#679](https://github.com/khuisman/mcp-gee-sweet/issues/679))
333
- - [ ] `list_files`/`get_file_metadata` docstrings cite a stale `upload_local_file` `modifiedTime` caveat (fixed at the root by PR #472) and carry a broken self-referential contrast — from Bob's `docs/tools.md` docstring sweep; Dev/QA track, batches with #682/#683 ([#681](https://github.com/khuisman/mcp-gee-sweet/issues/681))
333
+ - [x] `list_files`/`get_file_metadata` docstrings cite a stale `upload_local_file` `modifiedTime` caveat (fixed at the root by PR #472) and carry a broken self-referential contrast — from Bob's `docs/tools.md` docstring sweep; Dev/QA track, batches with #682/#683 ([#681](https://github.com/khuisman/mcp-gee-sweet/issues/681)) (PR 898)
334
334
  - [ ] `create_doc` family docstrings name only the ~25MP inline-image limit; the ~50MB byte-size limit (#562) is equally enforced, and 3 deferring docstrings inherit the gap — from Bob's `docs/tools.md` docstring sweep ([#682](https://github.com/khuisman/mcp-gee-sweet/issues/682))
335
335
  - [ ] `qa(docs)`: no comprehensive fixture exercising every Docs formatting construct in one pass — Aziz, triaged out of `backlog` 2026-09-11 ([#477](https://github.com/khuisman/mcp-gee-sweet/issues/477))
336
336
  - [ ] 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))
@@ -92,6 +92,7 @@ from .auth import ( # noqa: E402
92
92
  set_raise_auth_failures,
93
93
  spreadsheet_lifespan,
94
94
  )
95
+ from .sse_shutdown import SingleResponseGuard # noqa: E402
95
96
 
96
97
 
97
98
  def _parse_enabled_tools() -> set | None:
@@ -124,8 +125,10 @@ mcp = MCPServer(
124
125
  )
125
126
 
126
127
  # mcp v2 moved host/port from the constructor to call-time kwargs on
127
- # sse_app()/run_sse_async()/run() itself (confirmed live against mcp==2.0.0, issue #175)
128
- app = mcp.sse_app(host=_resolved_host)
128
+ # sse_app()/run_sse_async()/run() itself (confirmed live against mcp==2.0.0, issue #175).
129
+ # SingleResponseGuard keeps a SIGTERM with an open SSE stream from logging an ASGI
130
+ # traceback (#868); main() serves this app for every SSE launch so the guard applies.
131
+ app = SingleResponseGuard(mcp.sse_app(host=_resolved_host))
129
132
 
130
133
 
131
134
  _tool_access_logger = logging.getLogger("mcp_gee_sweet.access")
@@ -597,6 +600,18 @@ def main():
597
600
  # the connection without Google access instead (PR #867).
598
601
  set_raise_auth_failures(True)
599
602
  mcp.run(transport=transport)
603
+ elif transport == "sse":
604
+ # mcp.run(transport="sse") would build a second, unguarded sse_app, so serve
605
+ # the guarded module-level app with run_sse_async()'s own launch (#868):
606
+ # anyio.run on the plain asyncio loop, which #833's auth handoff was built
607
+ # and QA'd on. uvicorn.run() would pick uvloop whenever it's installed.
608
+ import anyio
609
+ import uvicorn
610
+
611
+ config = uvicorn.Config(
612
+ app, host=_resolved_host, port=_resolved_port, log_level=mcp.settings.log_level.lower()
613
+ )
614
+ anyio.run(uvicorn.Server(config).serve)
600
615
  else:
601
616
  # mcp v2 moved host/port from the constructor to call-time kwargs (see the
602
617
  # mcp.sse_app() call above) — stdio's own overload doesn't accept them.
@@ -0,0 +1,60 @@
1
+ """ASGI guard for the SSE stream a server shutdown cuts off (#868).
2
+
3
+ On SIGTERM, sse_starlette's ``EventSourceResponse`` sees ``AppStatus.should_exit``
4
+ and cancels its own task group, so its closing ``http.response.body``
5
+ (``more_body=False``) is never sent: the response is left started but incomplete.
6
+ mcp's ``sse_app`` endpoint then returns an empty ``Response()``, which Starlette
7
+ sends on the same connection, and uvicorn rejects that second
8
+ ``http.response.start`` with "Exception in ASGI application". A client disconnect
9
+ doesn't hit this, because uvicorn turns every send after a disconnect into a no-op.
10
+
11
+ ``SingleResponseGuard`` drops that late second response, but only once
12
+ ``AppStatus.should_exit`` is set. If the first one is still open, it sends the
13
+ missing closing body first, so the client gets a properly terminated stream
14
+ instead of a truncated one. Before shutdown every message passes through
15
+ untouched, so uvicorn still reports a genuine double response (PR #911 QA round 1).
16
+ """
17
+
18
+ import logging
19
+
20
+ from sse_starlette.sse import AppStatus
21
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class SingleResponseGuard:
27
+ """During shutdown, drop a second ``http.response.start`` on a request that already started one."""
28
+
29
+ def __init__(self, app: ASGIApp) -> None:
30
+ self.app = app
31
+
32
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
33
+ if scope["type"] != "http":
34
+ await self.app(scope, receive, send)
35
+ return
36
+
37
+ started = False
38
+ complete = False
39
+ discarding = False
40
+
41
+ async def guarded_send(message: Message) -> None:
42
+ nonlocal started, complete, discarding
43
+ if discarding:
44
+ return
45
+ if message["type"] == "http.response.start":
46
+ if started and AppStatus.should_exit:
47
+ discarding = True
48
+ logger.debug(
49
+ "Dropping a second response on %s during shutdown (#868)", scope.get("path")
50
+ )
51
+ if not complete:
52
+ complete = True
53
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
54
+ return
55
+ started = True
56
+ elif message["type"] == "http.response.body" and not message.get("more_body", False):
57
+ complete = True
58
+ await send(message)
59
+
60
+ await self.app(scope, receive, guarded_send)
@@ -560,10 +560,9 @@ def register(tool):
560
560
  starred status, and md5_checksum. md5_checksum is only present for
561
561
  binary files — Google Workspace files (Docs, Sheets, Slides, etc.) have
562
562
  no fixed byte content and the Drive API omits the field for them, so
563
- it's None for those. Use it to detect real content drift (e.g. after
564
- sync_folder's upload path, which doesn't stamp modifiedTime the way
565
- sync_folder's own upload does) instead of inferring change from
566
- modifiedTime alone.
563
+ it's None for those. Use it to detect real content drift (for example,
564
+ a local overwrite that preserves mtime) instead of inferring change
565
+ from modifiedTime alone.
567
566
  Results are cached; call refresh_cache(folder_id=folder_id) to invalidate,
568
567
  or refresh_cache() to clear all caches.
569
568
  """
@@ -774,9 +773,8 @@ def register(tool):
774
773
  md5_checksum (like size) is only present for binary files — Google
775
774
  Workspace files have no fixed byte content and the Drive API omits it
776
775
  for them. Use it to detect real content drift instead of inferring
777
- change from modifiedTime alone (e.g. after upload_local_file, which
778
- doesn't stamp modifiedTime to match a local file's mtime the way
779
- sync_folder does). starred reflects star_file/unstar_file's own effect
776
+ change from modifiedTime alone (for example, a local overwrite that
777
+ preserves mtime). starred reflects star_file/unstar_file's own effect
780
778
  (#388) — previously there was no way to read a file's starred state
781
779
  independently of calling one of those two mutating tools.
782
780
  """
@@ -8,7 +8,9 @@ import sys
8
8
  from types import SimpleNamespace
9
9
  from unittest.mock import MagicMock
10
10
 
11
+ import anyio
11
12
  import pytest
13
+ import uvicorn
12
14
  from google.auth.exceptions import RefreshError
13
15
  from googleapiclient import errors as googleapiclient_errors
14
16
  from googleapiclient.errors import HttpError
@@ -30,6 +32,7 @@ from mcp_gee_sweet.server import (
30
32
  mcp,
31
33
  tool,
32
34
  )
35
+ from mcp_gee_sweet.sse_shutdown import SingleResponseGuard
33
36
 
34
37
 
35
38
  def _http_error(status: int, message: str) -> HttpError:
@@ -727,11 +730,33 @@ class TestMainAuthAndTransport:
727
730
 
728
731
  def test_sse_keeps_interactive_consent(self, monkeypatch):
729
732
  monkeypatch.setattr(sys, "argv", ["mcp-gee-sweet", "--transport", "sse"])
730
- monkeypatch.setattr(mcp, "run", MagicMock())
733
+ monkeypatch.setattr(anyio, "run", MagicMock())
731
734
  main()
732
735
  assert auth_module._interactive_consent is True
733
736
  assert auth_module._raise_auth_failures is False
734
737
 
738
+ def test_sse_serves_the_guarded_app_like_run_sse_async(self, monkeypatch):
739
+ # #868: mcp.run(transport="sse") builds its own app without SingleResponseGuard,
740
+ # so main() serves the guarded app with run_sse_async()'s own launch:
741
+ # anyio.run(uvicorn.Server(config).serve), not uvicorn.run (which picks uvloop).
742
+ monkeypatch.setattr(sys, "argv", ["mcp-gee-sweet", "--transport", "sse"])
743
+ monkeypatch.setattr(server, "_resolved_host", "127.0.0.9")
744
+ monkeypatch.setattr(server, "_resolved_port", 47123)
745
+ monkeypatch.setattr(mcp.settings, "log_level", "DEBUG")
746
+ mcp_run, uvicorn_run, anyio_run = MagicMock(), MagicMock(), MagicMock()
747
+ monkeypatch.setattr(mcp, "run", mcp_run)
748
+ monkeypatch.setattr(uvicorn, "run", uvicorn_run)
749
+ monkeypatch.setattr(anyio, "run", anyio_run)
750
+ main()
751
+ mcp_run.assert_not_called()
752
+ uvicorn_run.assert_not_called()
753
+ (serve,) = anyio_run.call_args.args
754
+ assert serve.__func__ is uvicorn.Server.serve
755
+ config = serve.__self__.config
756
+ assert isinstance(server.app, SingleResponseGuard)
757
+ assert config.app is server.app
758
+ assert (config.host, config.port, config.log_level) == ("127.0.0.9", 47123, "debug")
759
+
735
760
 
736
761
  class TestMainLogsVersion:
737
762
  """Issue #356: main() logs the running package version at startup so a
@@ -0,0 +1,189 @@
1
+ """Tests for SingleResponseGuard: a SIGTERM with an open SSE stream (#868)."""
2
+
3
+ import threading
4
+
5
+ import anyio
6
+ import pytest
7
+ from mcp.server.mcpserver import MCPServer
8
+ from sse_starlette import sse as sse_starlette_sse
9
+ from sse_starlette.sse import AppStatus
10
+
11
+ from mcp_gee_sweet.sse_shutdown import SingleResponseGuard
12
+
13
+ _HTTP_SCOPE = {"type": "http", "method": "GET", "path": "/sse"}
14
+
15
+
16
+ class _StrictSend:
17
+ """Records sends, enforcing the ordering checks of uvicorn's h11 ``send``."""
18
+
19
+ def __init__(self):
20
+ self.messages = []
21
+ self.started = False
22
+ self.complete = False
23
+
24
+ async def __call__(self, message):
25
+ if not self.started:
26
+ if message["type"] != "http.response.start":
27
+ raise RuntimeError(
28
+ f"Expected ASGI message 'http.response.start', but got '{message['type']}'."
29
+ )
30
+ self.started = True
31
+ elif not self.complete:
32
+ if message["type"] != "http.response.body":
33
+ raise RuntimeError(
34
+ f"Expected ASGI message 'http.response.body', but got '{message['type']}'."
35
+ )
36
+ if not message.get("more_body", False):
37
+ self.complete = True
38
+ else:
39
+ raise RuntimeError(
40
+ f"Unexpected ASGI message '{message['type']}' sent, after response already completed."
41
+ )
42
+ self.messages.append(message)
43
+
44
+
45
+ async def _never_receive():
46
+ await anyio.sleep_forever()
47
+
48
+
49
+ def _app_sending(*messages):
50
+ async def app(scope, receive, send):
51
+ for message in messages:
52
+ await send(message)
53
+
54
+ return app
55
+
56
+
57
+ _START = {"type": "http.response.start", "status": 200, "headers": []}
58
+ _CHUNK = {"type": "http.response.body", "body": b"data: x\n\n", "more_body": True}
59
+ _END = {"type": "http.response.body", "body": b"", "more_body": False}
60
+
61
+
62
+ class TestSingleResponseGuard:
63
+ @pytest.fixture
64
+ def shutting_down(self, monkeypatch):
65
+ monkeypatch.setattr(AppStatus, "should_exit", True)
66
+
67
+ @pytest.fixture(autouse=True)
68
+ def _not_shutting_down_by_default(self, monkeypatch):
69
+ monkeypatch.setattr(AppStatus, "should_exit", False)
70
+
71
+ @pytest.mark.parametrize("should_exit", [False, True])
72
+ async def test_a_single_response_passes_through_untouched(self, monkeypatch, should_exit):
73
+ monkeypatch.setattr(AppStatus, "should_exit", should_exit)
74
+ send = _StrictSend()
75
+ await SingleResponseGuard(_app_sending(_START, _CHUNK, _END))(
76
+ _HTTP_SCOPE, _never_receive, send
77
+ )
78
+ assert send.messages == [_START, _CHUNK, _END]
79
+
80
+ @pytest.mark.parametrize(
81
+ "messages",
82
+ [(_START, _CHUNK, _START, _END), (_START, _END, _START, _END)],
83
+ ids=["open", "complete"],
84
+ )
85
+ async def test_before_shutdown_a_second_response_still_reaches_the_server(self, messages):
86
+ # PR #911 QA round 1: outside shutdown a double response is a real bug, so
87
+ # uvicorn must still see (and report) it, and an open stream isn't closed.
88
+ send = _StrictSend()
89
+ with pytest.raises(RuntimeError, match=r"'http\.response\.start'"):
90
+ await SingleResponseGuard(_app_sending(*messages))(_HTTP_SCOPE, _never_receive, send)
91
+ assert send.messages == list(messages[:2])
92
+
93
+ @pytest.mark.usefixtures("shutting_down")
94
+ async def test_a_second_response_on_an_open_stream_closes_it_and_is_dropped(self):
95
+ # The #868 shape: the stream is cut off without its closing body, then
96
+ # mcp's empty Response() starts a second response on the same connection.
97
+ send = _StrictSend()
98
+ late_response = ({"type": "http.response.start", "status": 200, "headers": []}, _END)
99
+ app = _app_sending(_START, _CHUNK, *late_response)
100
+ await SingleResponseGuard(app)(_HTTP_SCOPE, _never_receive, send)
101
+ assert send.messages == [_START, _CHUNK, _END]
102
+ assert send.complete
103
+
104
+ @pytest.mark.usefixtures("shutting_down")
105
+ async def test_a_second_response_after_a_complete_one_is_dropped(self):
106
+ send = _StrictSend()
107
+ app = _app_sending(_START, _END, _START, _END)
108
+ await SingleResponseGuard(app)(_HTTP_SCOPE, _never_receive, send)
109
+ assert send.messages == [_START, _END]
110
+
111
+ @pytest.mark.usefixtures("shutting_down")
112
+ async def test_other_misuse_still_reaches_the_server(self):
113
+ # Only a duplicate start is absorbed, even during shutdown; a body after
114
+ # completion still raises.
115
+ send = _StrictSend()
116
+ app = _app_sending(_START, _END, _CHUNK)
117
+ with pytest.raises(RuntimeError, match="after response already completed"):
118
+ await SingleResponseGuard(app)(_HTTP_SCOPE, _never_receive, send)
119
+
120
+ async def test_non_http_scopes_pass_straight_through(self):
121
+ seen = []
122
+
123
+ async def app(scope, receive, send):
124
+ seen.append((scope, send))
125
+
126
+ def send(message): # never wrapped, so never awaited
127
+ raise AssertionError
128
+
129
+ scope = {"type": "lifespan"}
130
+ await SingleResponseGuard(app)(scope, _never_receive, send)
131
+ assert seen == [(scope, send)]
132
+
133
+
134
+ class TestShutdownWithOpenSseStream:
135
+ """mcp's real sse_app, shut down the way uvicorn's SIGTERM handler does."""
136
+
137
+ @pytest.fixture(autouse=True)
138
+ def _fresh_shutdown_state(self, monkeypatch):
139
+ monkeypatch.setattr(AppStatus, "should_exit", False)
140
+ # sse_starlette's shutdown watcher is per thread; don't inherit another test's.
141
+ monkeypatch.setattr(sse_starlette_sse, "_thread_state", threading.local())
142
+
143
+ @staticmethod
144
+ async def _serve_then_shut_down(app):
145
+ send = _StrictSend()
146
+ scope = {
147
+ "type": "http",
148
+ "asgi": {"version": "3.0"},
149
+ "http_version": "1.1",
150
+ "method": "GET",
151
+ "scheme": "http",
152
+ "path": "/sse",
153
+ "raw_path": b"/sse",
154
+ "root_path": "",
155
+ "query_string": b"",
156
+ "headers": [(b"host", b"127.0.0.1:8000"), (b"accept", b"text/event-stream")],
157
+ "client": ("127.0.0.1", 50000),
158
+ "server": ("127.0.0.1", 8000),
159
+ }
160
+ errors = []
161
+
162
+ async def serve():
163
+ try:
164
+ await app(scope, _never_receive, send)
165
+ except Exception as exc: # what uvicorn logs as "Exception in ASGI application"
166
+ errors.append(exc)
167
+
168
+ with anyio.fail_after(10):
169
+ async with anyio.create_task_group() as tg:
170
+ tg.start_soon(serve)
171
+ while not send.messages: # the stream is open once its endpoint event is out
172
+ await anyio.sleep(0.01)
173
+ AppStatus.should_exit = True
174
+ return send, errors
175
+
176
+ async def test_unguarded_app_reproduces_the_traceback(self):
177
+ # Pins the premise: without the guard, mcp's sse_app hits uvicorn's check.
178
+ _, errors = await self._serve_then_shut_down(MCPServer("t").sse_app(host="0.0.0.0"))
179
+ assert len(errors) == 1
180
+ assert "but got 'http.response.start'" in str(errors[0])
181
+
182
+ async def test_guarded_app_shuts_down_cleanly(self):
183
+ app = SingleResponseGuard(MCPServer("t").sse_app(host="0.0.0.0"))
184
+ send, errors = await self._serve_then_shut_down(app)
185
+ assert errors == []
186
+ starts = [m for m in send.messages if m["type"] == "http.response.start"]
187
+ assert len(starts) == 1
188
+ assert send.messages[-1] == _END
189
+ assert send.complete