mcp-gee-sweet 0.9.2.dev243__tar.gz → 0.9.2.dev246__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 (253) hide show
  1. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/CLAUDE.md +8 -2
  2. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/PKG-INFO +2 -2
  3. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/README.md +1 -1
  4. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/auth.md +12 -3
  5. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/client-setup.md +1 -1
  6. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/index.md +1 -0
  7. mcp_gee_sweet-0.9.2.dev246/docs/design/oauth-reauthorize-from-tool-call.md +81 -0
  8. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/infra.md +208 -2
  9. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/roadmap.md +2 -1
  10. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/auth.py +402 -83
  11. mcp_gee_sweet-0.9.2.dev246/src/mcp_gee_sweet/http_transport.py +102 -0
  12. mcp_gee_sweet-0.9.2.dev246/src/mcp_gee_sweet/reauth.py +212 -0
  13. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/server.py +61 -25
  14. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/conftest.py +7 -2
  15. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_auth.py +4 -2
  16. mcp_gee_sweet-0.9.2.dev246/tests/test_reauth.py +699 -0
  17. mcp_gee_sweet-0.9.2.dev243/src/mcp_gee_sweet/http_transport.py +0 -42
  18. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/cleanup-worktrees.md +0 -0
  19. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/merge-pr.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/notify-kai.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/notify-partner.md +0 -0
  22. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/orchestrator.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/prep-for-pr.md +0 -0
  24. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/qa-kickoff.md +0 -0
  25. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/release.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/retro.md +0 -0
  27. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/roadmap-status.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/start-worktree.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/team-member.md +0 -0
  30. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/commands/verify-pr.md +0 -0
  31. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/settings.json +0 -0
  32. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/amy.md +0 -0
  33. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/aziz.md +0 -0
  34. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/bob.md +0 -0
  35. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/dev.md +0 -0
  36. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/joy.md +0 -0
  37. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/kai.md +0 -0
  38. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.claude/team-roles/qa.md +0 -0
  39. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.dockerignore +0 -0
  40. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  41. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  42. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/dependabot.yml +0 -0
  44. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/workflows/ci.yml +0 -0
  45. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/workflows/docs.yml +0 -0
  46. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/workflows/publish-dev.yml +0 -0
  47. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.github/workflows/release.yml +0 -0
  48. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.gitignore +0 -0
  49. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.pre-commit-config.yaml +0 -0
  50. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/.python-version +0 -0
  51. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/CHANGELOG.md +0 -0
  52. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/CODE_OF_CONDUCT.md +0 -0
  53. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/CONTRIBUTING.md +0 -0
  54. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/Dockerfile +0 -0
  55. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/LICENSE +0 -0
  56. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/Makefile +0 -0
  57. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/SECURITY.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docker-compose.yml +0 -0
  59. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/assets/logo.png +0 -0
  60. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/configuration.md +0 -0
  61. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-async-tool-execution.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-cache-invalidation.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-chart-theming.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-comments-first-class.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-composite-tools.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-dev-team.md +0 -0
  70. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-docs-formatting.md +0 -0
  71. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-fork.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-gmail-domain.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  77. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-prompt-qa-role.md +0 -0
  78. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-publishing.md +0 -0
  79. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-release-docs-roles.md +0 -0
  80. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-repositioning.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-testing.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/decision-tool-error-visibility.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/decisions/index.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/blockquote-representation.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/borderless-table-columns.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/doc-to-markdown.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/docs-ast-pipeline.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/effectiveformat-spike.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/heading-anchor-resolution.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/image-conversion.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/markdown-support.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/module-history.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/phase3-theme-system.md +0 -0
  99. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design/sse-consent-off-event-loop.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/design.md +0 -0
  101. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/index.md +0 -0
  102. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/known-limitations.md +0 -0
  103. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/notes-read.md +0 -0
  104. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/README.md +0 -0
  105. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  106. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  107. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  108. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  109. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  110. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  111. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  112. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  113. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  114. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  115. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  116. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  117. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  118. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  119. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  120. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  121. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  122. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  123. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/fixtures.template.md +0 -0
  124. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/gmail-test-plan.md +0 -0
  125. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/operations.yaml +0 -0
  126. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/playwright_oauth.md +0 -0
  127. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/.gitkeep +0 -0
  128. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-02.md +0 -0
  129. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-14.md +0 -0
  130. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-17.md +0 -0
  131. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-18.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  133. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  134. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-06-28.md +0 -0
  135. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-07-04-docs.md +0 -0
  136. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-07-04-infra.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-07-04.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/results/2026-09-04.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/retro-v0.8.0.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/retro-v0.9.0.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/run.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/runs/README.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/runs/v0.8.0.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/runs/v0.8.1.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/runs/v0.9.0.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/setup.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/calendar.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/docs_content.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/docs_layout.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/docs_style.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/docs_tables.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/drive_activity.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/drive_files.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/drive_sharing.md +0 -0
  157. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/drive_transfer.md +0 -0
  158. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/gmail.md +0 -0
  159. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/sheets_charts.md +0 -0
  160. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/sheets_mgmt.md +0 -0
  161. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/sheets_read.md +0 -0
  162. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/qa/tests/sheets_write.md +0 -0
  163. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/style-guide.md +0 -0
  164. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/docs/tools.md +0 -0
  165. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/mkdocs.yml +0 -0
  166. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/pyproject.toml +0 -0
  167. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/pyrightconfig.json +0 -0
  168. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/check_supports_all_drives.py +0 -0
  169. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/gen_tool_docs.py +0 -0
  170. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/lane_context_hook.py +0 -0
  171. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/oauth_setup.py +0 -0
  172. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/qa_cancel_insert_local_images.py +0 -0
  173. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/qa_gmail_fixtures.py +0 -0
  174. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/setup_team.sh +0 -0
  175. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/scripts/write_team_mcp_configs.sh +0 -0
  176. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/server.json +0 -0
  177. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/.env.template +0 -0
  178. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/__init__.py +0 -0
  179. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/cache.py +0 -0
  180. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
  181. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  182. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/cache.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  190. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  191. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  193. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  194. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  197. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  198. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  199. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/pagination.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/__init__.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/drive/__init__.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/drive/test_activity.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/drive/test_files.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/drive/test_sharing.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/drive/test_transfer.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/fakes.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/integration/__init__.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/integration/conftest.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/integration/test_local_fs_harness.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/sheets/__init__.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/sheets/test_data.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/sheets/test_helpers.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/sheets/test_structure.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_cache.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_calendar.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_check_supports_all_drives.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_concurrency.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_anchors.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_comments.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_content.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_core.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_editing.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_images.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_indices.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_layout.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_markdown_export.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_named_ranges.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_style.py +0 -0
  243. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_docs_tables.py +0 -0
  244. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_gen_tool_docs.py +0 -0
  245. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_gmail.py +0 -0
  246. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_lane_context_hook.py +0 -0
  247. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_qa_gmail_fixtures.py +0 -0
  248. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_response_limits.py +0 -0
  249. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_server.py +0 -0
  250. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_server_json.py +0 -0
  251. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_sse_shutdown.py +0 -0
  252. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/tests/test_team_mcp_configs.py +0 -0
  253. {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev246}/uv.lock +0 -0
@@ -90,14 +90,20 @@ Each entry below says what a module owns and the invariants a future edit could
90
90
 
91
91
  **The lifespan runs once per connection under SSE, not once per process.** mcp v2's `Server.run` enters `spreadsheet_lifespan` for every SSE connection; stdio has exactly one. So per-connection auth outcomes belong on the `SpreadsheetContext` it yields, never in a module global that a later connection's lifespan can overwrite or reset. PR #828 (#811) round 1 kept its degraded-start message in a global, and QA caught a second connection clearing the message the first still relied on. Genuinely process-wide decisions (e.g. "don't retry the consent flow", the shared `_consent_attempt`) are the exception, and should say so in a comment.
92
92
 
93
- **No usable OAuth token (#811):** under stdio the server never runs the browser consent (stdout is the JSON-RPC channel). `AUTH_METHOD=oauth`, or the waterfall with no service account/ADC fallback, starts *degraded*: the yielded context has every service `None` and `unauthorized_message` set, and `server.py`'s `_timed` wrapper raises that message before any tool body runs. It raises rather than returns, because a `{"error": ...}` dict would fail a list-typed tool's output schema. Under SSE the consent runs with its prompt on stderr and an `OAUTH_CONSENT_TIMEOUT_SECONDS` bound, and any failure degrades that connection. The recovery path users are pointed at is the `mcp-gee-sweet auth` subcommand (`auth.run_auth_command`, argv parsing in `server._auth_main`). `scripts/oauth_setup.py` is only a QA wrapper over the same `run_consent_flow`/`write_token_json` (0600) helpers.
93
+ **No usable OAuth token (#811):** under stdio the lifespan never starts a consent (stdout is the JSON-RPC channel). `AUTH_METHOD=oauth`, or the waterfall with no service account/ADC fallback, starts *degraded*: the yielded context has every service `None` and `unauthorized_message` set, and on that connection each tool call first offers re-authorization instead of running (#873, below); `server.py`'s `_timed` wrapper raises the degraded message itself only when no offer can be made. It raises rather than returns, because a `{"error": ...}` dict would fail a list-typed tool's output schema. Under SSE the consent runs with its prompt on stderr and an `OAUTH_CONSENT_TIMEOUT_SECONDS` bound, and any failure degrades that connection. The recovery paths are the tool call's consent offer (#873) and the `mcp-gee-sweet auth` subcommand (`auth.run_auth_command`, argv parsing in `server._auth_main`). `scripts/oauth_setup.py` is only a QA wrapper over the same `run_consent_flow`/`write_token_json` (0600) helpers.
94
94
 
95
95
  **The SSE auth phase runs off the event loop (#833).** Design and the races behind each rule: `docs/design/sse-consent-off-event-loop.md`. Invariants:
96
96
  - Nothing in the lifespan's auth phase runs on the loop. `_oauth_creds_async` runs the token load/refresh and the consent on daemon threads (`_run_in_daemon_thread`), never `asyncio.to_thread`: interpreter shutdown joins executor threads, which would move the hang to exit. Every wait goes through `_await_unless_shutdown`, which polls sse_starlette's `AppStatus.should_exit`, the only shutdown signal a lifespan can see. Without it, uvicorn waits on the connection and SIGTERM hangs until the wait ends.
97
- - One consent attempt at a time, process-wide on purpose: `_consent_attempt` is shared, and connections arriving during it await the same future. A failed attempt turns consent off in `_ConsentAttempt._on_settle`, on the loop, before any waiter sees the failure, and consent is never retried in the process (each new connection would wait out the timeout again).
97
+ - One consent attempt at a time, process-wide on purpose: `_consent_attempt` is shared, and connections arriving during it await the same future. A failed *lifespan* attempt turns consent off in `_ConsentAttempt._on_settle`, on the loop, before any waiter sees the failure, and the lifespan never retries consent in the process (each new connection would wait out the timeout again). A tool call's user-started consent is exempt from both (see #873 below).
98
98
  - A token load runs on its own thread, so it can race an attempt that settles meanwhile. On the loop, re-check `_interactive_consent` before starting an attempt, and reload the token if `_consent_successes` changed during the load. Don't reuse a finished attempt's credentials: a load that starts after a success and still finds no usable token needs a new consent.
99
99
  - Outside stdio the lifespan never raises: `spreadsheet_lifespan` turns any auth failure into a degraded connection. A lifespan that raises after an `await` leaves a POST that raced in parked forever in mcp's SSE transport, and SIGTERM hangs on it (python-sdk#3616). Only stdio (`set_raise_auth_failures(True)` in `server.main()`) keeps #790's fail-fast exit.
100
100
 
101
+ **A tool call re-authorizes OAuth in place (#873).** Design: `docs/design/oauth-reauthorize-from-tool-call.md`. Invariants:
102
+ - A non-retryable `RefreshError` is detected at `thread_http`'s transport (`_RefreshTrackingHttp`), not in tool code: most tools catch it and return `{"error": ...}` (#906). `_timed` runs every call inside `track_refresh_failures()` and replaces the result with `reauth.after_refresh_failure`'s offer. Any new request path must go through `thread_http`.
103
+ - The offer is `UrlElicitationRequiredError` when the client declares `elicitation.url`, else `reauth.ReauthorizationRequired` (a `ToolError`) carrying the URL. A tool call never opens a browser.
104
+ - A tool call's consent shares the process-wide `_consent_attempt` with the lifespan's, but is `user_started`: it ignores `_interactive_consent` (so it works under stdio), no departing waiter stops it, and its failure doesn't turn consent off. A tool call may join a lifespan's attempt (marking it `offered`, so it isn't stopped), but a lifespan never waits on a tool call's: it degrades at once with `OAuthConsentRequiredError(disables_consent=False)` (PR #925 QA round 1). A nearly expired tool-call attempt is replaced, not stopped: it keeps listening to its own deadline (`join_group_of`), because a client holds its link, and whichever of the two completes notifies both attempts' clients (PR #925 QA round 2).
105
+ - New credentials go through `publish_oauth_credentials`, which alone sets the Gmail gate for them; a reload runs `_oauth_creds(record_gmail=False)`. Each context adopts a newer generation in place (`adopt_published_credentials`) on its next call. A mid-session failure that adopts a token asks for a retry instead of re-running the tool, and the offer carries the tool's own result (`describe_outcome`).
106
+
101
107
  **CodeQL flags logging any value whose name looks like a secret.** `py/clear-text-logging-sensitive-data` is a *high* alert that blocks merge. It keys on names, so logging a raw `OAUTH_CONSENT_TIMEOUT_SECONDS` value (an integer) tripped it on PR #828 after QA had already passed. Don't interpolate the value of an `OAUTH_*`/`*TOKEN*`/`*SECRET*`/`*PASSWORD*`-named env var or variable into a log message; name the setting without echoing it.
102
108
 
103
109
  The project philosophy is to be as powerful and flexible as possible by default, while giving security-conscious users the configuration knobs to lock things down. Two mechanisms reflect this:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gee-sweet
3
- Version: 0.9.2.dev243
3
+ Version: 0.9.2.dev246
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
@@ -118,7 +118,7 @@ export TOKEN_PATH="/path/to/token.json"
118
118
  mcp-gee-sweet auth # one-time browser login; `uvx mcp-gee-sweet auth` for a PyPI install
119
119
  ```
120
120
 
121
- Pass the same `CREDENTIALS_PATH` and `TOKEN_PATH` in your MCP client's `env`. A stdio server with no token doesn't open a browser itself; its tools return an error telling you to run `mcp-gee-sweet auth`.
121
+ Pass the same `CREDENTIALS_PATH` and `TOKEN_PATH` in your MCP client's `env`. A stdio server with no token doesn't open a browser itself. Instead its first tool call offers you the Google consent link (or you run `mcp-gee-sweet auth`), and you retry the call. A token revoked mid-session is handled the same way, with no restart.
122
122
 
123
123
  Service accounts (recommended for headless server deployments — see Option B above), base64 credential injection, and Application Default Credentials are also supported. See [Authentication](https://khuisman.github.io/mcp-gee-sweet/latest/auth/) for all options.
124
124
 
@@ -83,7 +83,7 @@ export TOKEN_PATH="/path/to/token.json"
83
83
  mcp-gee-sweet auth # one-time browser login; `uvx mcp-gee-sweet auth` for a PyPI install
84
84
  ```
85
85
 
86
- Pass the same `CREDENTIALS_PATH` and `TOKEN_PATH` in your MCP client's `env`. A stdio server with no token doesn't open a browser itself; its tools return an error telling you to run `mcp-gee-sweet auth`.
86
+ Pass the same `CREDENTIALS_PATH` and `TOKEN_PATH` in your MCP client's `env`. A stdio server with no token doesn't open a browser itself. Instead its first tool call offers you the Google consent link (or you run `mcp-gee-sweet auth`), and you retry the call. A token revoked mid-session is handled the same way, with no restart.
87
87
 
88
88
  Service accounts (recommended for headless server deployments — see Option B above), base64 credential injection, and Application Default Credentials are also supported. See [Authentication](https://khuisman.github.io/mcp-gee-sweet/latest/auth/) for all options.
89
89
 
@@ -50,9 +50,18 @@ Authenticates as you — gives full access to your personal Drive. Requires a on
50
50
 
51
51
  This opens the Google consent page, waits for you to finish, and writes the token to `TOKEN_PATH`, replacing any token already there. It requests the scopes the registered tools need, so `--include-tools` / `ENABLED_TOOLS` narrow it the same way they narrow the server.
52
52
 
53
- **Without a usable token, a stdio server doesn't open a browser itself.** Under stdio, stdout is the protocol channel and the client gives up on a slow start, so the server can't show a consent prompt. It starts without Google access instead: every tool returns an error with the `mcp-gee-sweet auth` instructions, and `server://auth-status` reports `auth_method: "none"` with an `oauth_not_authorized` limitation. In the waterfall, the server first tries a service account and ADC, and only starts without access if neither is available. If a service account or ADC was configured on purpose (`SERVICE_ACCOUNT_PATH` or `GOOGLE_APPLICATION_CREDENTIALS` set) but wasn't usable, the error says why. Run `mcp-gee-sweet auth`, then restart the server. An expired token whose refresh Google rejects (for example, a revoked refresh token) is handled the same way. If Google simply can't be reached at startup (say, the network isn't up yet), the server keeps the token and refreshes it on the first tool call.
53
+ **Without a usable token, a stdio server doesn't open a browser itself.** Under stdio, stdout is the protocol channel and the client gives up on a slow start, so the server can't show a consent prompt. It starts without Google access instead, and `server://auth-status` reports `auth_method: "none"` with an `oauth_not_authorized` limitation. In the waterfall, the server first tries a service account and ADC, and only starts without access if neither is available. If a service account or ADC was configured on purpose (`SERVICE_ACCOUNT_PATH` or `GOOGLE_APPLICATION_CREDENTIALS` set) but wasn't usable, the error says why. An expired token whose refresh Google rejects at startup (for example, a revoked refresh token) is handled the same way. If Google simply can't be reached at startup (say, the network isn't up yet), the server keeps the token and refreshes it on the first tool call.
54
54
 
55
- Over SSE, the server still runs the browser consent itself when there's no usable token. The prompt goes to stderr, and the server waits at most `OAUTH_CONSENT_TIMEOUT_SECONDS` (default 5 minutes). The rest of the server keeps working during the wait. Connections that open during it wait for the same consent instead of starting another one, and stopping the server (SIGTERM, Ctrl+C) ends the wait. If consent is denied, fails, or isn't finished by then, the connections waiting on it start without Google access as described above. The server doesn't try the browser consent again for later connections, so an unattended server doesn't make each new connection wait out the timeout. Each new connection re-reads `TOKEN_PATH` instead, so after `mcp-gee-sweet auth` a reconnect is enough.
55
+ **Re-authorizing from the client.** When a tool call finds no usable token, or Google rejects a token refresh mid-session, the call offers the Google consent page itself, without a restart:
56
+
57
+ - A client that supports MCP URL elicitation (Claude Code does) gets a prompt with the consent link. The server never opens a browser on its own: you choose to open the link.
58
+ - Any other client gets an error whose text carries the link.
59
+
60
+ Open the link, approve access, and retry the call. The server saves the token to `TOKEN_PATH` (mode `0600`) and the retried call uses it, on every connection. The link works for `OAUTH_CONSENT_TIMEOUT_SECONDS` (default 5 minutes). Calls that fail while it's open get the same link, along with how long it still works, and a call after it expires gets a new one. A call whose token is rejected partway through isn't re-run, since it may have already done part of its work: its error includes what the tool itself reported. This works over stdio and SSE alike. It applies to OAuth only: a service account or ADC connection reports a rejected refresh as an ordinary error.
61
+
62
+ `mcp-gee-sweet auth` still works as the alternative, for example when the client can't show the link. Run it with the same settings as the server, then retry the call. The server re-reads `TOKEN_PATH` on the next call, so no restart is needed.
63
+
64
+ Over SSE, the server still runs the browser consent itself when there's no usable token. The prompt goes to stderr, and the server waits at most `OAUTH_CONSENT_TIMEOUT_SECONDS` (default 5 minutes). The rest of the server keeps working during the wait. Connections that open during it wait for the same consent instead of starting another one, and stopping the server (SIGTERM, Ctrl+C) ends the wait. If consent is denied, fails, or isn't finished by then, the connections waiting on it start without Google access as described above. The server doesn't start that browser consent again for later connections, so an unattended server doesn't make each new connection wait out the timeout. Those connections still offer the consent link from their first tool call, as described above.
56
65
 
57
66
  `mcp-gee-sweet auth` checks that the `TOKEN_PATH` directory exists and is writable before it opens the consent page, and saves the token readable by your user only (mode `0600`). The server rewrites it the same way when it refreshes.
58
67
 
@@ -61,7 +70,7 @@ Over SSE, the server still runs the browser consent itself when there's no usabl
61
70
  - **Only the Gmail scope is missing** (for example, a token from before the Gmail tools existed): the server starts normally and Sheets, Drive, Docs, Calendar and activity tools keep working. Every Gmail tool returns an error naming the missing scope and how to re-authorize, and `server://auth-status` lists the Gmail tools under a `gmail_not_authorized` limitation.
62
71
  - **Any other scope is missing:** under stdio, startup stops with an error naming the missing scopes. Most tools can't work without them. (Under a stdio client this error goes to stderr, which the client usually doesn't show; the client just reports that the connection closed. Set `DEBUG_LEVEL` and `LOG_FILE` to capture it in a file.) Over SSE, the server keeps running and the connection starts without Google access: every tool returns that error, with the instructions to re-authorize. The same applies to any other authentication failure over SSE.
63
72
 
64
- Either way, run `mcp-gee-sweet auth` (see step 4 above) and restart the server to re-authorize.
73
+ Either way, run `mcp-gee-sweet auth` (see step 4 above) and restart the server to re-authorize. A tool call doesn't offer the consent link for a missing scope.
65
74
 
66
75
  **Gmail scope is only requested when a Gmail tool is registered.** With no `ENABLED_TOOLS` filter every tool is registered, so the server asks for `gmail.modify` (read, compose, send, label, trash). If you don't want to grant mailbox access, leave the Gmail tools out of `ENABLED_TOOLS` / `--include-tools`. The server then neither requests nor requires the Gmail scope, and an existing pre-Gmail token keeps working.
67
76
 
@@ -28,7 +28,7 @@ Add a block under `mcpServers` in `claude_desktop_config.json`. Choose stdio (cl
28
28
 
29
29
  Replace `/path/to/mcp-gee-sweet` with your clone path. This uses OAuth (the default waterfall's first method, full personal Drive access) — swap the `env` block for a different auth method — see [Authentication](auth.md).
30
30
 
31
- Before the first connection, log in once from a terminal with the same paths: `CREDENTIALS_PATH=... TOKEN_PATH=... uv run --directory /path/to/mcp-gee-sweet mcp-gee-sweet auth`. A stdio server with no token doesn't open a browser itself. It starts without Google access, and its tools return an error telling you to run this command.
31
+ Before the first connection, log in once from a terminal with the same paths: `CREDENTIALS_PATH=... TOKEN_PATH=... uv run --directory /path/to/mcp-gee-sweet mcp-gee-sweet auth`. A stdio server with no token doesn't open a browser itself. It starts without Google access, and its first tool call offers you the Google consent link, or you can run this command and retry (see [Re-authorizing from the client](auth.md#method-b-oauth-20-personal-use--local-dev)).
32
32
 
33
33
  ### SSE — Docker
34
34
 
@@ -13,3 +13,4 @@ These are more granular than the [decision records](../decisions/index.md), whic
13
13
  | [Blockquote Representation](blockquote-representation.md) | 2026-08-06 | Flat `blockquote_depth` field (mirroring `BulletItem.depth`) vs. a wrapper node; left border + scaled indent via `paragraphStyle.borderLeft`, live-verified writable before implementing |
14
14
  | [Module Implementation History](module-history.md) | 2026-09-29 | Per-PR history behind root `CLAUDE.md`'s module and workflow rules, moved there verbatim by #849 so `CLAUDE.md` keeps only lasting rules and invariants |
15
15
  | [SSE OAuth Consent Off the Event Loop](sse-consent-off-event-loop.md) | 2026-10-01 | #833: the SSE consent wait on a daemon thread with a stoppable callback server, shared across connections, ended by sse_starlette's shutdown flag on SIGTERM |
16
+ | [Re-authorizing OAuth from the Failing Tool Call](oauth-reauthorize-from-tool-call.md) | 2026-10-06 | #873/#906: a revoked or missing token offers the consent link from the failing call (URL elicitation, or the link in the `ToolError`), detected at `thread_http`'s transport so tools that catch errors are covered, adopted in place on every connection |
@@ -0,0 +1,81 @@
1
+ # Re-authorizing OAuth from the Failing Tool Call
2
+
3
+ **Issue:** #873 (folds in #906) · **Date:** 2026-10-06 · **Status:** implemented
4
+
5
+ ## Problem
6
+
7
+ Before this change, an OAuth server with no usable token stayed broken until someone ran a terminal command and restarted it:
8
+
9
+ - **Degraded start (#811).** With no usable token at startup, a stdio server starts without Google access. Every tool returned "run `mcp-gee-sweet auth`, then restart the server or reconnect".
10
+ - **Mid-session refresh failure.** The token was revoked or expired after a good start. #811 didn't cover this. PR #905 added a re-authorize hint in `_timed`, but most tools catch the `RefreshError` in their own `except Exception` and return `{"error": ...}` before `_timed` sees it (#906). So `list_files` showed a raw tuple repr with no hint, while `list_sheets`, which lets the error propagate, showed the hint.
11
+
12
+ The goal is to recover in place, without the server opening a browser unprompted (one of #811's complaints), and without the model having to know about a separate tool.
13
+
14
+ ## Client support (checked first, as the issue asked)
15
+
16
+ A probe server logged the `initialize` capabilities of Claude Code 2.1.291 as `{"elicitation": {"form": {}, "url": {}}, "roots": {...}}`, protocol `2026-07-28`. So URL-mode elicitation is the main path, and the `ToolError` text is the fallback for other clients.
17
+
18
+ Under `claude -p` (no human), a tool raising `UrlElicitationRequiredError` came back to the model as "URL elicitation was canceled by the user". A headless client never shows the link. That's the client's behavior, and the CLI alternative still covers it.
19
+
20
+ ## Design
21
+
22
+ ### Mechanism: `UrlElicitationRequiredError`, not an in-call `ctx.elicit_url`
23
+
24
+ The tool call raises `UrlElicitationRequiredError` (JSON-RPC `-32042`). mcp's `Tool.run` re-raises an `MCPError` as a protocol error instead of wrapping it as a tool result.
25
+
26
+ The in-call alternative, `ctx.elicit_url()`, sends a server-to-client request and keeps the call open while the user decides. It would hold the call for up to `OAUTH_CONSENT_TIMEOUT_SECONDS`, and it needs a back-channel, which a 2026-07-28 request context may lack (`NoBackChannelError`). The error form returns immediately, and the consent runs on independently of any call.
27
+
28
+ When the consent completes, the server sends `notifications/elicitation/complete` to every session it offered the URL to. This is best effort: a gone session or a missing channel is logged at debug level and ignored.
29
+
30
+ ### Noticing a rejected refresh in every tool
31
+
32
+ The issue comment pointed at `execute_in_thread` as the single choke point. It isn't quite one: media downloads (`MediaIoBaseDownload.next_chunk`) set `request.http = thread_http(service)` and never go through `execute_in_thread`. Every Google request does go through `thread_http`'s transport, so that's where the hook sits. `_RefreshTrackingHttp` (an `AuthorizedHttp` subclass) records a non-retryable `RefreshError` on a per-call `CallAuthState` before re-raising it.
33
+
34
+ `_timed` opens the state with `track_refresh_failures()`, a `ContextVar` holding a mutable object. Gathered tasks and `asyncio.to_thread` workers copy the context, and the copy points at the same object, so a failure on any of them is seen. After the tool body returns or raises, a recorded failure replaces whatever the tool produced (including an `{"error": ...}` dict) with the re-authorization offer. The 86 tool-level `except` blocks stay untouched.
35
+
36
+ A retryable `RefreshError` (the token endpoint's own 5xx) isn't recorded: it says nothing about the token.
37
+
38
+ ### Recovery order (`reauth.py`)
39
+
40
+ Both entry points, `before_call` (degraded connection) and `after_refresh_failure` (mid-session), try in order:
41
+
42
+ 1. **Adopt a token this process already got.** `publish_oauth_credentials` bumps a process-wide generation. A context built from an older generation rebuilds its services in place (`apply_oauth_credentials`) on its next call. One `TOKEN_PATH` means one token for every connection.
43
+ 2. **Reload `TOKEN_PATH`** (`load_saved_token`, on a daemon thread like the rest of the auth code). This picks up a token `mcp-gee-sweet auth` wrote, so the CLI no longer needs a restart. In the mid-session case a reloaded token with the *same* refresh token as the failing one is rejected: it's the same revoked grant. Two guards (PR #925 QA round 1):
44
+ - **The reload doesn't touch the Gmail gate.** It runs `_oauth_creds(record_gmail=False)`, so the process-wide `_gmail_unauthorized_message` isn't reset and re-set by every connection's reload, whether or not anything gets adopted. `publish_oauth_credentials` sets the gate from the token actually adopted.
45
+ - **An unchanged file isn't reloaded again.** After a reload finds nothing to adopt, `TOKEN_PATH`'s `(mtime_ns, size)` is remembered, and the reload is skipped until the file changes. Without this, a revoked token on disk cost a failing refresh round-trip and a warning on every call. `mcp-gee-sweet auth` rewrites the file, so it's still picked up.
46
+ 3. **Offer consent.** Start or join the shared listener and raise the elicitation, or the `ToolError` with the URL. The offer states the time the link actually has left (`_ConsentAttempt.remaining_seconds`), not the full timeout. A tool call's own attempt with less than `min(60, timeout/2)` seconds left is replaced by a fresh one rather than handed out. The replaced attempt isn't stopped: a client already holds its link, and its offer promised that link until its deadline. In PR #925's second round it was stopped, and QA reproduced a link dying with 7.9s of its stated time left. So two listeners can overlap, for at most `min(60, timeout/2)` seconds. The two attempts share a `group`, and whichever completes publishes the token and sends `elicitation/complete` to both attempts' clients.
47
+
48
+ When a degraded connection adopts a token, the tool body then runs normally. When a mid-session failure adopts one, the call ends with "a newer token was loaded; retry the call". The tool isn't re-run automatically, because a multi-request tool may already have made changes before the refresh failed. So the offer carries what the tool itself reported (`describe_outcome`, truncated at 1,500 characters): "The call may have partly completed before the token was rejected; the tool reported: ...". The model then knows what may already have happened.
49
+
50
+ ### One listener per process, shared with the lifespan
51
+
52
+ `start_user_consent` reuses `_consent_attempt`, the slot #833 made process-wide. So a tool call that arrives during an SSE lifespan consent offers *that* consent's URL (`_serve_consent`'s new `on_url` callback records it), and marks it `offered`: once a client holds the link, the last lifespan waiter leaving no longer stops the attempt and closes its port. `_ConsentListener` splits binding the callback server and building the URL from the wait, so the URL exists before anything blocks. The build runs on a daemon thread. If the call is cancelled during it, a done callback closes the listener once it's bound. That's a callback rather than an `await` in cleanup, so a cancel scope's re-delivered cancellation can't skip it.
53
+
54
+ The reverse doesn't join. A lifespan that needs consent while a tool call's attempt is pending doesn't wait on it: nothing would be shown to anyone for that wait. It starts without access at once, raising `OAuthConsentRequiredError(disables_consent=False)` so degrading doesn't turn consent off, and the connection's first tool call offers the same link. In PR #925's first round the lifespan did join. QA reproduced the result over SSE: a connection waited out the whole timeout with no prompt, then turned server consent off for every later connection.
55
+
56
+ A tool call's attempt (`user_started`) differs from a lifespan's in three ways:
57
+
58
+ - It never opens a browser and prints no stderr prompt; the client shows the URL.
59
+ - No waiter's departure stops it, since the call that started it has already returned. It runs to its timeout.
60
+ - Its failure doesn't turn off `_interactive_consent`.
61
+
62
+ ### Restart wording
63
+
64
+ `reauthorize_instructions(restart=False)` ends "then retry the call: the server picks up the new token without a restart". It's used everywhere the token is missing or rejected: the degraded-start messages, `server://auth-status`, and the offer's CLI alternative. `mcp-gee-sweet auth` prints the same. Only the missing-scope messages (#790, including Gmail's) still say to restart, since nothing reloads the token there.
65
+
66
+ `_client_error` keeps its re-authorize hint for a non-retryable `RefreshError` that reaches it without going through the offer. A *retryable* one (the token endpoint's own 5xx) gets no hint, and it is reported as a plain error.
67
+
68
+ ### The issue's open questions
69
+
70
+ - **SSE's "consent is never retried" rule.** It still governs the server's *own* consent in the lifespan, which an unattended server shouldn't keep restarting for every connection. A tool-call consent is started by a user, from a client, one at a time. Its failure only means the next failing call offers a fresh URL. `start_user_consent` deliberately ignores `_interactive_consent`, which is also what lets it run under stdio.
71
+ - **`mcp-gee-sweet auth`'s role.** It becomes the secondary path. It's still documented in `docs/auth.md` and still named in every offer, for clients that can't show the link. It's still the only fix for a missing-scope token (below).
72
+
73
+ ### `thread_http` cache
74
+
75
+ Rebuilding the services makes new service objects, and CPython can reuse a freed object's `id()`. The per-thread transport cache was keyed on `id(service)` alone, so a rebuilt service could get a stale transport holding the revoked credentials. A cached transport is now reused only while `http.credentials is service._http.credentials`.
76
+
77
+ ## Limitations
78
+
79
+ - **Missing scopes aren't offered.** A token lacking a base scope still stops a stdio server at startup (#790), and a Gmail-only shortfall still degrades only the Gmail tools. Both still need `mcp-gee-sweet auth` and a restart.
80
+ - **The callback listens on `localhost`.** The browser that completes the consent must run on the server's machine. This already applied to the lifespan's own SSE consent; it matters more now that a remote SSE client can be shown the link.
81
+ - **A headless client cancels the elicitation.** The user never sees the link (see above).
@@ -792,7 +792,7 @@ Then call `list_spreadsheets` with `max_results: 1`, and read `server://auth-sta
792
792
 
793
793
  **Checks**
794
794
  - `initialize` completes within a few seconds, with no browser window and no `Please visit` text (the stdio stream stays valid JSON-RPC)
795
- - `list_spreadsheets` returns an error result whose text says there's no usable OAuth token at `<nonexistent path>`, and tells you to run `mcp-gee-sweet auth` (and `uvx mcp-gee-sweet auth` for a PyPI install) with the same `TOKEN_PATH`/`CREDENTIALS_PATH`/`ENABLED_TOOLS`, then restart
795
+ - `list_spreadsheets` returns an error result whose text says there's no usable OAuth token at `<nonexistent path>`. Since #873 (a client with no elicitation callback, as here) the text then gives a `https://accounts.google.com/...` consent link, and the alternative of running `mcp-gee-sweet auth` (and `uvx mcp-gee-sweet auth` for a PyPI install) with the same `TOKEN_PATH`/`CREDENTIALS_PATH`/`ENABLED_TOOLS`, then retrying the call. It no longer says to restart.
796
796
  - `auth-status` reports `auth_method: "none"`, `limited_tools: ["*"]`, and one `oauth_not_authorized` limitation
797
797
  - `<tmp log>` has a `WARNING ... Starting without Google access:` line and a `"TOOL list_spreadsheets" 401` access line
798
798
  - `<nonexistent path>` still does not exist
@@ -805,6 +805,12 @@ Real `mcp` SDK `stdio_client` + `ClientSession`. `initialize` OK in 1.9s, 0 `Ple
805
805
  **Result (2026-09-26, PR #828 round 2 @ f633c4c, Sky) ✅ PASS**
806
806
  Same as round 1: `initialize` 1.8s, error result with the instructions (now ending `then restart the server or reconnect to it.`), `auth-status` `none` / `oauth_not_authorized`, `"TOOL list_spreadsheets" 401`, 0 `Please visit`. Gate now reads the connection's own `lifespan_context.unauthorized_message`.
807
807
 
808
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ✅ PASS**
809
+ `initialize` 1.9s, 0 `Please visit` in the log. `list_spreadsheets` error text: `No usable OAuth token at '<nonexistent>'. To re-authorize, open this link in a browser and approve access within <N>s, then retry the call: https://accounts.google.com/o/oauth2/auth?...`, then `Or run \`mcp-gee-sweet auth\` ... (\`uvx mcp-gee-sweet auth\` ...) ... then retry the call: the server picks up the new token without a restart.` No restart instruction in the tool text. `auth-status`: `none`, `["*"]`, one `oauth_not_authorized`. The log has `Starting without Google access:` and `"TOOL list_spreadsheets" 401`. The token path still doesn't exist. (`auth-status`'s own `reason`/`alternatives` still say `then restart the server or reconnect to it`, which is review finding 9, not this case's check.)
810
+
811
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
812
+ `initialize` 1.7s, 0 `Please visit`. `list_spreadsheets`: `No usable OAuth token at '<nonexistent>'.`, then the link, then the CLI alternative ending `then retry the call: the server picks up the new token without a restart.`. No restart wording. `auth-status` `none` / `["*"]` / one `oauth_not_authorized`. The log has `Starting without Google access:` and `"TOOL list_spreadsheets" 401`. The token path still doesn't exist.
813
+
808
814
  ---
809
815
 
810
816
  ### TC-I36: stdio waterfall with no token falls through to a service account, and starts without access only if nothing else works (issue #811) ⚠️ local-filesystem
@@ -1094,7 +1100,7 @@ ENABLED_TOOLS=get_storage_quota
1094
1100
  Call `get_storage_quota` with no arguments.
1095
1101
 
1096
1102
  **Checks**
1097
- - The result has `is_error: true`, and its text is `Error executing tool get_storage_quota: No usable OAuth token at '<scratch token>' ...`, followed by the `mcp-gee-sweet auth` / `uvx mcp-gee-sweet auth` instructions naming the same `TOKEN_PATH` and `CREDENTIALS_PATH`
1103
+ - The result has `is_error: true`, and its text is `Error executing tool get_storage_quota: No usable OAuth token at '<scratch token>' ...`, followed by the `mcp-gee-sweet auth` / `uvx mcp-gee-sweet auth` instructions naming the same `TOKEN_PATH` and `CREDENTIALS_PATH`. Since #873 a consent link comes before those instructions (see TC-I48–TC-I52); this case only checks that the text reaches the client.
1098
1104
  - The text is not only `Error executing tool get_storage_quota`
1099
1105
 
1100
1106
  **Cleanup:** delete the scratch files.
@@ -1105,6 +1111,12 @@ mcp 2.3.0. Startup refresh failed (`invalid_client`) and the server started degr
1105
1111
  **Result (2026-10-04, PR #905 round 2 @ 42ab5e6, Sky) ✅ PASS**
1106
1112
  Same as round 1 after the degraded start began raising `ToolError` directly: `is_error: True`, identical `mcp-gee-sweet auth` text, access line `401`.
1107
1113
 
1114
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ✅ PASS**
1115
+ mcp 2.3.0, bogus scratch client JSON, expired token with a bogus refresh token. `get_storage_quota`: `is_error: True`, text starts `Error executing tool get_storage_quota: No usable OAuth token at '<scratch token>'.`, then the consent link (built from the bogus client ID), then the `mcp-gee-sweet auth` / `uvx mcp-gee-sweet auth` alternative naming the scratch `TOKEN_PATH` and `CREDENTIALS_PATH`. Not the bare `Error executing tool get_storage_quota`.
1116
+
1117
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
1118
+ mcp 2.3.0. Same as round 1: `is_error: True`, starts `Error executing tool get_storage_quota: No usable OAuth token at '<scratch token>'.`, with the `mcp-gee-sweet auth` / `uvx mcp-gee-sweet auth` alternative naming the scratch paths. Not the bare text.
1119
+
1108
1120
  ---
1109
1121
 
1110
1122
  ### TC-I46: on mcp 2.3+, a Google API error a tool raises keeps its text, for a tool and for `spreadsheet://{id}/info` (issue #872)
@@ -1158,3 +1170,197 @@ uvicorn 0.47.0, run through `uv run` (so the parent's exit code is uv's 143 afte
1158
1170
 
1159
1171
  **Result (2026-10-05, PR #911 round 2 @ 1f138a5, Sky) ✅ PASS**
1160
1172
  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`.
1173
+
1174
+ ---
1175
+
1176
+ ### TC-I48: a token revoked mid-session offers the consent link from tools that catch the error and from tools that raise it (issue #873/#906) ⚠️ requires-oauth ⚠️ local-filesystem
1177
+
1178
+ **Background:** most tools catch a `RefreshError` in their own `except Exception` and return `{"error": ...}`, so #905's re-authorize hint in `_timed` never reached them (#906). `thread_http`'s transport now records a rejected refresh on the call, and `_timed` replaces the tool's result with the re-authorization offer. A client with no URL-elicitation support gets the link in the `ToolError` text. The text uses the error's `args[0]`, not the `RefreshError` tuple's repr.
1179
+
1180
+ **Setup**
1181
+ - A real OAuth client JSON (`CREDENTIALS_PATH`) and a working `token.json` for it.
1182
+ - A scratch copy of that token (mode `0600`) with `token` set to `ya29.invalid`, `refresh_token` set to `1//invalid`, and `expiry` one hour in the future. Startup then accepts the token without refreshing, and the first API call gets a 401, refreshes, and Google answers `invalid_grant`.
1183
+ - Run `refresh_cache` first, or use IDs not in the shared `/tmp/mcp_gee_sweet.db` cache: a cache hit makes no API call, so no refresh happens.
1184
+
1185
+ **Action**
1186
+ Run the server under the `mcp` SDK's `stdio_client` + `ClientSession`, with **no** `elicitation_callback`, and with `DEBUG_LEVEL=INFO`, `LOG_FILE=<tmp log>`:
1187
+
1188
+ ```
1189
+ AUTH_METHOD=oauth
1190
+ TOKEN_PATH=<scratch token>
1191
+ CREDENTIALS_PATH=<oauth client json>
1192
+ OAUTH_CONSENT_TIMEOUT_SECONDS=120
1193
+ BROWSER=/usr/bin/false
1194
+ ```
1195
+
1196
+ 1. Call `list_files` with `folder_id: "root"`, `max_results: 1` (catches the error itself)
1197
+ 2. Call `list_sheets` with any real spreadsheet ID (lets the error propagate)
1198
+
1199
+ **Checks**
1200
+ - 1 and 2: an error result whose text starts `Error executing tool <name>: Google rejected the OAuth token refresh: invalid_grant: ...`, then `To re-authorize, open this link in a browser and approve access within <N>s, then retry the call: https://accounts.google.com/o/oauth2/auth?...` (`<N>` at most 120: the time the link has left), then the `mcp-gee-sweet auth` alternative naming the scratch `TOKEN_PATH` and `CREDENTIALS_PATH`, ending `then retry the call: the server picks up the new token without a restart.`
1201
+ - The part of either text before `The call may have partly completed` has no `{'error'` (the tuple repr)
1202
+ - 1 only (round 2): the text ends `The call may have partly completed before the token was rejected; the tool reported: {"error": "List files failed: ..."}`, the tool's own result, which the offer replaced. 2 has no such sentence (the tool raised the refresh error itself)
1203
+ - Run step 1 three more times: the log has only one more `Token refresh failed` warning in total (the unchanged `TOKEN_PATH` isn't reloaded again), and no `Gmail tools disabled` line from these calls
1204
+ - Both calls give the *same* link. Its `redirect_uri` is `http://localhost:<port>/`, and a TCP connect to that port succeeds (one listener, still open)
1205
+ - No browser opened, and no `Please visit this URL` line in stderr or the log
1206
+ - `<tmp log>` has `"TOOL list_files" 401` and `"TOOL list_sheets" 401`
1207
+
1208
+ **Cleanup:** delete the scratch token and log.
1209
+
1210
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ✅ PASS**
1211
+ `stdio_client` + `ClientSession`, no elicitation callback, `CACHE_DB_PATH` pointed at a scratch file. Both `list_files` and `list_sheets`: `is_error: true`, text `Error executing tool <name>: Google rejected the OAuth token refresh: invalid_grant: Bad Request. To re-authorize, open this link in a browser and approve access within 120s, then retry the call: https://accounts.google.com/o/oauth2/auth?...`, then the `mcp-gee-sweet auth` alternative naming the scratch `TOKEN_PATH`/`CREDENTIALS_PATH`. No `{'error'` and no `List files failed`. Same link for both, `redirect_uri=http://localhost:60833/`, and a TCP connect to it succeeded. 0 `Please visit`. Log: `"TOOL list_files" 401 0.242s`, `"TOOL list_sheets" 401 7.408s`. Side observation: every token reload re-logs `WARNING ... Gmail tools disabled` (this token lacks `gmail.modify`), so each reload rewrites the process-wide Gmail gate (review finding 4).
1212
+
1213
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
1214
+ Both texts as specified, with time left `119s` and `118s` (not the full 120). The CLI alternative ends `... without a restart.`. 1 (`list_files`) ends `The call may have partly completed before the token was rejected; the tool reported: [{"error": "List files failed: ('invalid_grant: Bad Request', {...})"}]`. The tuple repr appears only inside that reported result, never before it. 2 (`list_sheets`) has no such sentence. Same link for both, and its port is open. Step 1 three more times: each `401` in 0.09s, 0 `Token refresh failed` lines in total (this token isn't expired, so the reload never refreshes, and the unchanged file is skipped), and only the startup `Gmail tools disabled` line (1 in the log, against one per reload in round 1). 0 `Please visit`.
1215
+
1216
+ ---
1217
+
1218
+ ### TC-I49: a client with URL elicitation gets a `-32042` error carrying the consent link (issue #873) ⚠️ requires-oauth ⚠️ local-filesystem
1219
+
1220
+ **Background:** a client that declares `elicitation.url` gets `UrlElicitationRequiredError`, a JSON-RPC protocol error, not a tool result. The client shows the link and asks the user before opening it. Claude Code declares this capability (2.1.291).
1221
+
1222
+ **Setup:** as TC-I48.
1223
+
1224
+ **Action:** as TC-I48, but pass an `elicitation_callback` to `ClientSession`, which makes the SDK declare `elicitation: {form, url}`. Catch the `MCPError` each call raises.
1225
+
1226
+ **Checks**
1227
+ - 1 and 2: the error's code is `-32042`. `UrlElicitationRequiredError.from_error(e.error).elicitations` has one entry, whose `url` is a `https://accounts.google.com/o/oauth2/auth?...` link and whose `message` starts `Google rejected the OAuth token refresh: invalid_grant: ...` and says to open the page and retry
1228
+ - Both calls carry the same `elicitation_id` and the same `url`
1229
+ - `<tmp log>` has `"TOOL list_files" 401` and `"TOOL list_sheets" 401`
1230
+
1231
+ **Cleanup:** delete the scratch token and log.
1232
+
1233
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ✅ PASS**
1234
+ With an `elicitation_callback` (returns cancel; never invoked, since the server raises rather than sending an in-call request). Both calls raised `MCPError` code `-32042`. `from_error(...).elicitations` had one entry each, with `url` `https://accounts.google.com/o/oauth2/auth?...` and `message` `Google rejected the OAuth token refresh: invalid_grant: Bad Request. Open this Google sign-in page to re-authorize mcp-gee-sweet (the link works for 120s), then retry.` Same `elicitation_id` and same `url` for both, and the callback port was open. Log: `"TOOL list_files" 401`, `"TOOL list_sheets" 401`.
1235
+
1236
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
1237
+ Same as round 1: both calls `-32042`, the same `elicitation_id` and `url`, port open, `401` access lines, 0 `Please visit`.
1238
+
1239
+ ---
1240
+
1241
+ ### TC-I50: after `mcp-gee-sweet auth` (or any new token in `TOKEN_PATH`), the next call recovers without a restart (issue #873) ⚠️ requires-oauth ⚠️ local-filesystem
1242
+
1243
+ **Background:** before #873, the CLI needed a server restart or reconnect afterwards. A failing call now re-reads `TOKEN_PATH` first, and adopts a usable token whose refresh token differs from the failing one.
1244
+
1245
+ **Setup:** as TC-I48, plus a second scratch path that doesn't exist yet.
1246
+
1247
+ **Action**
1248
+ 1. Start the TC-I48 client with `TOKEN_PATH=<nonexistent second path>` (a degraded start). Call `list_files` (`folder_id: "root"`, `max_results: 1`): expect the consent-link error. Without stopping the server, copy the working `token.json` to `<second path>`. Call `list_files` and `list_sheets` again
1249
+ 2. Start a fresh client with `TOKEN_PATH=<scratch token>` (revoked, as in TC-I48). Call `list_sheets`: expect the consent-link error. Copy the working `token.json` over `<scratch token>`. Call `list_sheets` twice more
1250
+
1251
+ **Checks**
1252
+ - 1: after the copy, both calls succeed. The log has `INFO mcp_gee_sweet.auth OAuth credentials reloaded; this connection is authorized again` once, and `200` access lines
1253
+ - 2: the first call after the copy returns an error ending `The server has since loaded a newer OAuth token; retry the call.` (a mid-session failure doesn't re-run the tool), and the next call succeeds with `200`
1254
+ - Neither server was restarted
1255
+
1256
+ **Cleanup:** delete the scratch tokens and logs.
1257
+
1258
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ✅ PASS**
1259
+ Step 1: the first `list_files` gave the consent-link error (`401`). After copying the working token in, `list_files` and `list_sheets` (`Sales Notes & Misc Empty`) both returned `200`, with one `INFO mcp_gee_sweet.auth OAuth credentials reloaded; this connection is authorized again`. Step 2: `list_sheets` gave the consent-link error. After the copy, the next call returned `... The server has since loaded a newer OAuth token; retry the call.` (`401`, one `reloaded` line), and the call after that returned `200`. Same server process throughout.
1260
+
1261
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
1262
+ Step 1: `401` with the link, then after the copy `OAuth credentials reloaded ...` once, `list_files` `200` and `list_sheets` `200`. Step 2: `401` with the link, then after the copy `... The server has since loaded a newer OAuth token; retry the call.` (`401`), then `200`. Side observation on step 2's first post-copy call: it now ends `The call may have partly completed ...; the tool reported: ["Sales", "Notes & Misc", "Empty"]`. `list_sheets` actually succeeded from the structure cache (the cache's `modifiedTime` check is where the revoked refresh failed and was recorded), and `_timed` replaced that success with the retry message. That's the design (a recorded failure replaces the result, now carried in the text), not a defect.
1263
+
1264
+ ---
1265
+
1266
+ ### TC-I51: completing the offered consent in a browser saves the token and the retried call succeeds (issue #873) ⚠️ requires-oauth ⚠️ local-filesystem
1267
+
1268
+ **Background:** the end-to-end path the issue asks for: the user opens the offered link, approves, and the callback saves the token. The token is written `0600`, and every connection adopts it on its next call. Needs a browser on the server's machine, signed in to the Google account (by hand, or Playwright under the `docs/qa/run.md` mutex).
1269
+
1270
+ **Setup:** as TC-I48, with `OAUTH_CONSENT_TIMEOUT_SECONDS=300`.
1271
+
1272
+ **Action**
1273
+ 1. As TC-I48 step 1: get the consent link from `list_files`
1274
+ 2. Open the link in the browser and approve every requested scope. The callback page reads `The authentication flow has completed. You may close this window.`
1275
+ 3. Call `list_files` again, then `list_sheets`
1276
+
1277
+ **Checks**
1278
+ - 3: both calls succeed
1279
+ - `<scratch token>` now has a different `refresh_token` from the bogus one and is mode `0600` (`stat -f %Lp` prints `600`)
1280
+ - The callback port from the link is closed after step 2
1281
+ - The server opened no browser itself at any point
1282
+
1283
+ **Cleanup:** revoke the new grant at https://myaccount.google.com/permissions if it was made for a scratch account, and delete the scratch token and log.
1284
+
1285
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ✅ PASS**
1286
+ `OAUTH_CONSENT_TIMEOUT_SECONDS=300`, a stdio session held open, and a fresh `CACHE_DB_PATH` (a cache shared with an earlier run served `list_files` with no API call, so no refresh happened: use a fresh cache for this case). The first `list_files` gave the link (`401`). Playwright, under the mutex, after the fixture-doc sign-in check: the account chooser, the Workspace account, then **Allow** led to `http://localhost:<port>/?state=...&code=...`, which read `The authentication flow has completed. You may close this window.` The scratch token's `refresh_token` changed and its mode is `0o600`. A TCP connect to the callback port then failed (closed). Log: `OAuth credentials reloaded ...`, then `list_files` `200` and `list_sheets` `200`. 0 `Please visit`, and no browser opened by the server (`BROWSER=/usr/bin/false`). The grant was on the fixture-owning Workspace account for the team's existing OAuth client, so nothing to revoke.
1287
+
1288
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
1289
+ Same flow as round 1 (Playwright under the mutex, Workspace account, **Allow**): the callback page completed, the scratch token's `refresh_token` changed with mode `0o600`, the callback port then closed, and `OAuth credentials reloaded ...` was followed by `list_files` `200` and `list_sheets` `200`. 0 `Please visit`.
1290
+
1291
+ ---
1292
+
1293
+ ### TC-I52: in Claude Code, a revoked token shows the consent prompt instead of an error (issue #873) ⚠️ requires-oauth ⚠️ local-filesystem
1294
+
1295
+ **Background:** the client-side half of TC-I49, in the client the team uses. Needs an interactive Claude Code session (`claude -p` cancels URL elicitation on its own).
1296
+
1297
+ **Setup:** register a stdio server in a scratch MCP config, using the TC-I48 scratch token and env, and start an interactive `claude --mcp-config <scratch config> --strict-mcp-config`.
1298
+
1299
+ **Action:** ask Claude to call that server's `list_files` with `folder_id: "root"`, `max_results: 1`. When the URL prompt appears, accept it, approve the consent in the browser that opens, then ask Claude to retry the call.
1300
+
1301
+ **Checks**
1302
+ - Claude Code shows a prompt with the `accounts.google.com` link and the elicitation message, before anything opens
1303
+ - After the consent, the retried call returns the file listing
1304
+ - Record whether Claude Code retried by itself after `notifications/elicitation/complete` or needed the explicit retry. Either is a pass; the behavior is the client's.
1305
+
1306
+ **Cleanup:** delete the scratch config, token, and log.
1307
+
1308
+ **Result (2026-10-06, PR #925 round 1 @ db4f13d, Sky) ⏭️ NOT RUN**
1309
+ Needs a human driving an interactive Claude Code session; a lane QA session can't accept the client's URL prompt itself. TC-I49 covers the wire format (`-32042` with the elicitation). Pending a human-driven run.
1310
+
1311
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ⏭️ NOT RUN**
1312
+ Same reason as round 1: needs a human in an interactive Claude Code session.
1313
+
1314
+ ---
1315
+
1316
+ ### TC-I53: over SSE, a connection opening while a tool call's consent link is pending starts at once, offers the same link, and leaves server consent on (PR #925 QA round 1) ⚠️ requires-oauth ⚠️ local-filesystem
1317
+
1318
+ **Background:** in round 1, connection B's lifespan joined the consent a tool call on connection A had offered. B waited the whole `OAUTH_CONSENT_TIMEOUT_SECONDS` with no prompt shown anywhere. It then degraded through `_disable_consent_after_failure`, so connection C reported `an earlier browser consent in this server process didn't complete`. Now a lifespan that finds a tool call's consent pending starts without access straight away (`disables_consent=False`), and its first tool call offers the same link. Separately, a tool call that offers a *lifespan's* link marks it `offered`, so the last lifespan waiter leaving no longer closes its callback port.
1319
+
1320
+ **Setup**
1321
+ - The TC-I48 scratch token (dead access token, bogus refresh token, expiry in the future), and a way to rewrite it with `expiry` one hour in the *past*.
1322
+ - A free port.
1323
+
1324
+ **Action**
1325
+ Start `uv run mcp-gee-sweet --transport sse` with `AUTH_METHOD=oauth`, `TOKEN_PATH=<scratch token>`, `CREDENTIALS_PATH=<oauth client json>`, `OAUTH_CONSENT_TIMEOUT_SECONDS=25`, `HOST=127.0.0.1`, `PORT=<port>`, `BROWSER=/usr/bin/false`, `DEBUG_LEVEL=INFO`, stdout and stderr to one log. With the `mcp` SDK's `sse_client` + `ClientSession` (no elicitation callback):
1326
+ 1. Connection A: `initialize`, call `list_files` (`folder_id: "root"`, `max_results: 1`), read `server://auth-status`
1327
+ 2. Rewrite the scratch token with an expired `expiry` (still the bogus refresh token)
1328
+ 3. Connection B: as A. Then connection C: as A
1329
+
1330
+ **Checks**
1331
+ - 1: the TC-I48 consent-link error
1332
+ - 3: B and C each finish `initialize` within a couple of seconds (not the 25s timeout), and their `list_files` error starts `No usable OAuth token at '<scratch token>'. To re-authorize, open this link ...` with the *same* link A got and a time left of at most 25s
1333
+ - No `auth-status` read's `oauth_not_authorized` limitation has `restart the server` in its text (a `gmail_not_authorized` one still says to restart, by design, if the token lacks `gmail.modify`)
1334
+ - The log has 0 `Please visit` lines, 0 `earlier browser consent` lines, and two `Starting without Google access: ... a re-authorization link a tool call offered is still open` lines (B and C)
1335
+
1336
+ **Cleanup:** SIGTERM the server; delete the scratch token and log.
1337
+
1338
+ **Result (2026-10-06, PR #925 round 2 @ a3af056, Sky) ✅ PASS**
1339
+ SSE on `127.0.0.1:8937`, `OAUTH_CONSENT_TIMEOUT_SECONDS=25`. A: `initialize` 0.2s, then the TC-I48 link with `within 24s`. After expiring the token: B and C each `initialize` in 0.1s (not 25s), and each `list_files` starts `No usable OAuth token at '<scratch token>'. To re-authorize, open this link ... within 24s` with the *same* link A got. The log has 0 `Please visit`, 0 `earlier browser consent`, and 2 `Starting without Google access: ... a re-authorization link a tool call offered is still open` lines. SIGTERM exited cleanly. B's and C's `auth-status` have no `restart the server`. A's does, from its `gmail_not_authorized` limitation: the team token lacks `gmail.modify`, and a missing scope still needs a restart by design. So I narrowed that check's wording to the `oauth_not_authorized` limitation.
1340
+
1341
+ ### TC-I54: replacing a nearly expired tool-call link leaves the old link working until its stated deadline (PR #925 QA round 2) ⚠️ requires-oauth ⚠️ local-filesystem
1342
+
1343
+ **Background:** a tool call's own consent attempt with less than `min(60, timeout/2)` seconds left isn't offered again; the next failing call gets a fresh link. In round 2 that replacement also stopped the old attempt, so link 1's callback port closed while its offer still promised it several more seconds, and a user on link 1's consent page landed on a dead localhost redirect. Now the old attempt keeps listening until its own deadline. The two attempts share a group: whichever completes publishes the token and sends `elicitation/complete` to both attempts' clients (unit-tested in `tests/test_reauth.py::TestUserConsent::test_either_link_completes_and_notifies_both_clients`).
1344
+
1345
+ **Setup**
1346
+ - The TC-I48 scratch token (dead access token, bogus refresh token).
1347
+
1348
+ **Action**
1349
+ Start a stdio session against `uv run mcp-gee-sweet` with `AUTH_METHOD=oauth`, `TOKEN_PATH=<scratch token>`, `CREDENTIALS_PATH=<oauth client json>`, `OAUTH_CONSENT_TIMEOUT_SECONDS=25` (replacement threshold 12s), `BROWSER=/usr/bin/false`, `DEBUG_LEVEL=INFO`, `LOG_FILE=<tmp log>`, using the `mcp` SDK's `stdio_client` + `ClientSession` (no elicitation callback):
1350
+ 1. Call `list_files` (`folder_id: "root"`, `max_results: 1`). Note the link (link 1), its `within <N>s`, and the time of the call; link 1's deadline is that time plus `<N>`
1351
+ 2. 16s after step 1, call `list_files` again. Note its link (link 2)
1352
+ 3. 2s after step 2, TCP-connect to link 1's and link 2's `redirect_uri` ports
1353
+ 4. 2s after link 1's deadline, TCP-connect to both ports again
1354
+
1355
+ **Checks**
1356
+ - 1: the TC-I48 consent-link error, `within 24s` or `within 25s`
1357
+ - 2: a *different* link, with a different `redirect_uri` port, and a time left of at least 23s
1358
+ - 3: both connects succeed (link 1 still open, with time left on its stated deadline)
1359
+ - 4: link 1's connect is refused (closed at its own deadline), and link 2's still succeeds
1360
+ - The log has 0 `Please visit` lines
1361
+
1362
+ **Cleanup:** end the session; delete the scratch token and log.
1363
+
1364
+ **Result (2026-10-08, PR #925 round 3 @ e5f6fed, Sky) ✅ PASS**
1365
+ mcp 2.3.0, stdio, fresh `CACHE_DB_PATH`. Step 1: `Google rejected the OAuth token refresh: invalid_grant ...`, link 1 `within 24s`, port 63319. Step 2 at +16.1s: a different link, port 63322, `within 24s`. Step 3 at +18.1s: both ports open (round 2 had link 1 closed by now). Step 4 at +26.0s (link 1's deadline +24s): link 1 refused, link 2 open. 0 `Please visit`.
1366
+ Extra check (the fix's "either link completes" claim, TC-I51-style): `OAUTH_CONSENT_TIMEOUT_SECONDS=120` (threshold 60s). Link 1 `within 119s`. At +61.6s a second `list_files` gave link 2 (a different port), with both ports open. Playwright, under the mutex, after the fixture-doc sign-in check, then completed consent through **link 1** (the replaced one): account chooser, the Workspace account, **Allow**, and the callback page on link 1's port read `The authentication flow has completed. You may close this window.` The scratch token's `refresh_token` changed and its mode is `0o600`. Link 1's port then closed, and the retried `list_files` succeeded. Link 2's port stayed open after the consent completed, as designed: it listens until its own deadline. 0 `Please visit`. Scratch files deleted.
@@ -214,7 +214,8 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
214
214
  - [x] auth: under `--transport sse`, the OAuth consent wait runs synchronously on the event loop, stalling every other session and ignoring SIGTERM until consent or timeout. #811 follow-up, found in PR #828 ([#833](https://github.com/khuisman/mcp-gee-sweet/issues/833)) (PR #867)
215
215
  - [x] Tool errors are opaque on PyPI installs: `mcp` 2.3 (what `uvx` resolves; the lock pins 2.0.0) hides any non-`ToolError` exception text, so #811's "run `mcp-gee-sweet auth`" message and uncaught Google API errors reach the client as a bare `Error executing tool <name>`. Observed live. Urgent: lane-a, next after #844 ([#872](https://github.com/khuisman/mcp-gee-sweet/issues/872)) (PR #905)
216
216
  - [ ] auth: offer OAuth re-authorization from the failing tool call, via URL-mode elicitation with the consent URL in a `ToolError` as fallback, then rebuild services in place. Covers a refresh token revoked mid-session too. No unprompted browser. Lane-a, after #872 ([#873](https://github.com/khuisman/mcp-gee-sweet/issues/873))
217
- - [ ] sheets: an A1 range with a dangling colon (`A1:`, `A:`, `5:`, `:5`, `:B`, `:B5`) passes `_parse_a1_notation` and widens through `_grid_range` to the whole sheet or a bigger rectangle. Hits every `_grid_range_or_error` tool; for `sort_range`/`merge_cells` it changes data. Found reviewing community PR #922, which rejects only a bare `":"`. Start after #922 merges (same lines) ([#926](https://github.com/khuisman/mcp-gee-sweet/issues/926))
217
+ - [ ] sheets: an A1 range with a dangling colon (`A1:`, `A:`, `5:`, `:5`, `:B`, `:B5`) passes `_parse_a1_notation` and widens through `_grid_range` to the whole sheet or a bigger rectangle. Hits every `_grid_range_or_error` tool; for `sort_range`/`merge_cells` it changes data. Found reviewing community PR #922, which rejects only a bare `":"` (merged). Also covers `1:B` and non-ASCII `.upper()` cases (see issue comment). Lane B, after #896 ([#926](https://github.com/khuisman/mcp-gee-sweet/issues/926))
218
+ - [ ] `get_doc_as_markdown` mangles inline code containing backticks. Community PR #923 (community-PR track, closes it on merge) ([#936](https://github.com/khuisman/mcp-gee-sweet/issues/936))
218
219
 
219
220
  **Sheets hardening** _(triaged out of `backlog` 2026-09-11 — see the "Backlog triage" note below)_
220
221
  - [x] `update_sheet_properties`/dimension-tool hardening + dedup follow-ups from PR #321's review ([#323](https://github.com/khuisman/mcp-gee-sweet/issues/323)) (PR #734)