mcp-gee-sweet 0.9.2.dev243__tar.gz → 0.9.2.dev248__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.
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/CLAUDE.md +8 -2
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/PKG-INFO +2 -2
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/README.md +1 -1
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/auth.md +12 -3
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/client-setup.md +1 -1
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/index.md +1 -0
- mcp_gee_sweet-0.9.2.dev248/docs/design/oauth-reauthorize-from-tool-call.md +81 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/gmail-test-plan.md +3 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/gmail.md +56 -1
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/infra.md +208 -2
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/roadmap.md +3 -2
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/auth.py +409 -83
- mcp_gee_sweet-0.9.2.dev248/src/mcp_gee_sweet/http_transport.py +102 -0
- mcp_gee_sweet-0.9.2.dev248/src/mcp_gee_sweet/reauth.py +212 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/server.py +61 -25
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/cache.py +2 -1
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/gmail.py +262 -114
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/conftest.py +7 -2
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_auth.py +4 -2
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_gmail.py +574 -2
- mcp_gee_sweet-0.9.2.dev248/tests/test_reauth.py +708 -0
- mcp_gee_sweet-0.9.2.dev243/src/mcp_gee_sweet/http_transport.py +0 -42
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/notify-kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/notify-partner.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/qa-kickoff.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/roadmap-status.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/amy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/bob.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/joy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.dockerignore +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.gitignore +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/.python-version +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/Dockerfile +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/LICENSE +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/Makefile +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/assets/logo.png +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-comments-first-class.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-gmail-domain.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-local-fs-test-harness.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-pillow-image-dependency.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-prompt-qa-role.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/decision-tool-error-visibility.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/blockquote-representation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/borderless-table-columns.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/doc-to-markdown.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/heading-anchor-resolution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/module-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design/sse-consent-off-event-loop.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/design.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/results/2026-09-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/retro-v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/runs/v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/docs_content.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/docs_layout.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/docs_style.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/docs_tables.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/drive_activity.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/drive_files.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/drive_sharing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/drive_transfer.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/style-guide.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/qa_cancel_insert_local_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/scripts/write_team_mcp_configs.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/server.json +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/pagination.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/fakes.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/integration/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/integration/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/integration/test_local_fs_harness.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_server_json.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/tests/test_team_mcp_configs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev243 → mcp_gee_sweet-0.9.2.dev248}/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
|
|
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
|
|
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.
|
|
3
|
+
Version: 0.9.2.dev248
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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).
|
|
@@ -128,6 +128,7 @@ Keep the IDs and intent. Rewrite each to name the exact tool and params and to r
|
|
|
128
128
|
| GM44 | `reply_to_message` (QA) → `list_messages` (sender) | End-to-end delivery. The QA mailbox replies to the `plain` fixture. On the sender side, `list_messages(query='subject:"[mcp-qa:plain]"')` shows the reply arrived in the **same thread** as the original. Proves `In-Reply-To`/`References` thread correctly in the recipient's mailbox, not just in ours. Plus-address cases can't show this. | write, cross-mailbox |
|
|
129
129
|
| GM45 | `send_message` (QA) → `get_message` (sender) | QA sends to the sender mailbox with `cc` = a QA plus-address and `bcc` = another. On the sender side, `get_message` shows `cc` present and **no** `bcc` header. Bcc must be stripped from delivered copies; GM39 only sees the sender's copy. | write, cross-mailbox |
|
|
130
130
|
| GM46 | `get_message` | `include_body=False` on the `attachments` fixture: headers, snippet, labels, and `size_estimate` present; no `body_plain`, `body_html`, or `attachments` key, since Gmail's `metadata` format returns no payload parts (#793) | read |
|
|
131
|
+
| GM47 | `send_message` → `get_message` | Attachments with no `mime_type` get a type guessed from the filename extension (`.pdf`, `.csv.gz` → `application/gzip`, unknown or `.eml` → `application/octet-stream`); an explicit `mime_type` still wins (#802) | write |
|
|
131
132
|
|
|
132
133
|
Every write case uses a `+tc-gmNN` plus-address and an `[mcp-qa:tc-gmNN]` subject (so `reset` sweeps it), and ends with a **Cleanup** step.
|
|
133
134
|
|
|
@@ -172,6 +173,8 @@ Found by reading `test_gmail.py` against `gmail.py` and running the helpers dire
|
|
|
172
173
|
|
|
173
174
|
P2 and P3 remain possible only for hand-built MIME, so they're unit-test material at most. P4–P9 are hardening tests. They can land together as one ticket.
|
|
174
175
|
|
|
176
|
+
**P4–P9 covered (#823, via #896):** `tests/test_gmail.py` now has unit tests for each. P9's `get_*` size-cap half had already landed with #825 (`TestMessageSizeCap`).
|
|
177
|
+
|
|
175
178
|
---
|
|
176
179
|
|
|
177
180
|
## 6. Execution order
|
|
@@ -63,6 +63,8 @@ The 7 `page-N` fixtures share the subject prefix `[mcp-qa:page]`, and each is it
|
|
|
63
63
|
- Step 6: all 7 messages, no `next_page_token`, no `error` (clamped down to 500)
|
|
64
64
|
- Don't check `result_size_estimate`. It's Gmail's estimate, not a count: live 2026-09-26, step 1 reported `201` for this 7-message query.
|
|
65
65
|
|
|
66
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS (regression, #795's shared `_list_kwargs`/`_list_page`)**: via `mcp-gee-sweet-kit`. `list_messages` and `list_threads` both paged 3/3/1 with `next_page_token` on the first two pages only, and the 7 IDs were distinct. Thread items carry `id`, `snippet`, and `history_id`. `max_results: 0` returned exactly 1 message, and `9999` returned all 7 with no token.
|
|
67
|
+
|
|
66
68
|
---
|
|
67
69
|
|
|
68
70
|
### TC-GM35: Spam and trash are excluded unless asked for
|
|
@@ -518,10 +520,14 @@ Uses a throwaway, not a fixture: removing `mcp-qa-fixture` from a fixture would
|
|
|
518
520
|
**Checks**
|
|
519
521
|
- Step 2: `body_plain` is `mcp-gee-sweet TC-GM38 plain`; `body_html` contains `<b>TC-GM38</b>`
|
|
520
522
|
- Step 2: `attachments` has exactly 2 entries: `filename: "mcp-qa-tc-gm38.csv"` (the file's own name, since no `filename` was passed) with `mime_type: "text/csv"`, and `filename: "note.txt"`
|
|
521
|
-
-
|
|
523
|
+
- Step 2: `note.txt`'s `mime_type` is `text/plain`, guessed from its extension since no `mime_type` was passed (#802; it was `application/octet-stream` before #896). TC-GM47 covers more extensions.
|
|
522
524
|
|
|
523
525
|
**Cleanup:** `trash_message` the step-1 `id`; delete `/tmp/mcp-qa-tc-gm38.csv`.
|
|
524
526
|
|
|
527
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS**: via `mcp-gee-sweet-kit`. Both bodies matched. There were 2 attachments: `mcp-qa-tc-gm38.csv` (`text/csv`, 18 bytes, read through `_compose_raw`'s off-loop thread) and `note.txt`, whose `mime_type` was `text/plain`, guessed. The message was trashed and the local file deleted.
|
|
528
|
+
|
|
529
|
+
**Result (2026-10-07, PR #928 round 2) ✅ PASS**: via `mcp-gee-sweet-kit` at `1d3c7cf`. Both bodies matched. There were 2 attachments: `mcp-qa-tc-gm38.csv` (`text/csv`, 18 bytes) and `note.txt` (`text/plain`, guessed, 18 bytes). The message was trashed and the local file deleted.
|
|
530
|
+
|
|
525
531
|
---
|
|
526
532
|
|
|
527
533
|
### TC-GM39: `cc` and `bcc` as lists ⚠️ destructive
|
|
@@ -554,6 +560,42 @@ Uses a throwaway, not a fixture: removing `mcp-qa-fixture` from a fixture would
|
|
|
554
560
|
|
|
555
561
|
---
|
|
556
562
|
|
|
563
|
+
### TC-GM47: An attachment without `mime_type` gets its type from the filename extension (issue #802) ⚠️ destructive
|
|
564
|
+
|
|
565
|
+
**Background:** an attachment passed without `mime_type` used to go out as `application/octet-stream` whatever its name, so recipients' clients couldn't preview it. #802 guesses the type from the filename's extension, using Python's built-in table only, so the result doesn't depend on the server host's own MIME database. A compressed file (`.gz`) gets the compression type, not the type of what's inside it. PR #928 round 2 added two rules. A guessed `message/*` type (`.eml`, `.mht`) falls back to `application/octet-stream`, since base64, which every attachment uses, is forbidden on `message/rfc822`. A guessed `text/*` type gets `charset=utf-8` when its bytes are valid UTF-8, and falls back to `application/octet-stream` when they aren't. `get_message` doesn't show the charset, so that part is unit-tested only (`TestBuildRawMessageAttachments`).
|
|
566
|
+
|
|
567
|
+
**Action**
|
|
568
|
+
1. `send_message` with `to: "{QA+tc-gm47}"`, `subject: "[mcp-qa:tc-gm47] attachment mime guess"`, `body: "mcp-gee-sweet TC-GM47"`, and `attachments` (none has `mime_type`):
|
|
569
|
+
- `{"content_base64": "JVBERi0xLjQK", "filename": "gm47.pdf"}`
|
|
570
|
+
- `{"content_base64": "aGVsbG8=", "filename": "gm47.csv.gz"}`
|
|
571
|
+
- `{"content_base64": "aGVsbG8=", "filename": "gm47.zzzunknown"}`
|
|
572
|
+
- `{"content_base64": "aGVsbG8=", "filename": "gm47.png", "mime_type": "text/plain"}` (explicit type wins)
|
|
573
|
+
- `{"content_base64": "RnJvbTogYUBleGFtcGxlLmNvbQ0KU3ViamVjdDogVEMtR000NyBpbm5lcg0KDQpoaQ0K", "filename": "gm47.eml"}` (a small RFC 822 message)
|
|
574
|
+
- `{"content_base64": "aMOpbGxvCg==", "filename": "gm47.txt"}` (UTF-8 `héllo`)
|
|
575
|
+
2. `get_message` with the `id` from step 1
|
|
576
|
+
|
|
577
|
+
**Checks**
|
|
578
|
+
- Step 2: `attachments` has exactly 6 entries, with `mime_type` by filename:
|
|
579
|
+
- `gm47.pdf` → `application/pdf`
|
|
580
|
+
- `gm47.csv.gz` → `application/gzip`
|
|
581
|
+
- `gm47.zzzunknown` → `application/octet-stream`
|
|
582
|
+
- `gm47.png` → `text/plain`
|
|
583
|
+
- `gm47.eml` → `application/octet-stream` (not `message/rfc822`)
|
|
584
|
+
- `gm47.txt` → `text/plain`
|
|
585
|
+
- No `error` field
|
|
586
|
+
|
|
587
|
+
**Cleanup:** `trash_message` the step-1 `id`.
|
|
588
|
+
|
|
589
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS (as written), with send-back findings out of scope for this case**: via `mcp-gee-sweet-kit`. The 4 attachments came back as `gm47.pdf` → `application/pdf`, `gm47.csv.gz` → `application/gzip`, `gm47.zzzunknown` → `application/octet-stream`, and `gm47.png` → `text/plain` (explicit wins), with no `error`. A probe in the same round showed two regressions the case's extensions don't reach. `fwd.eml` with no `mime_type` was guessed as `message/rfc822` but still built as a base64 `MIMEApplication`, which RFC 2046 §5.2.1 forbids. Python's parser decodes that payload to `None`. A UTF-8 `notes.txt` (`héllo`) was guessed as `text/plain` with no `charset`, so per RFC 2046 it reads as US-ASCII. Gmail accepted both. The part headers were confirmed by running the branch's `_build_raw_message` locally. Both messages were trashed.
|
|
590
|
+
|
|
591
|
+
**Result (2026-10-07, PR #928 round 2) ✅ PASS (as written), with a send-back finding out of scope for this case**: via `mcp-gee-sweet-kit`, after reconnecting to `1d3c7cf`. All 6 attachments came back as expected: `gm47.pdf` → `application/pdf`, `gm47.csv.gz` → `application/gzip`, `gm47.zzzunknown` → `application/octet-stream`, `gm47.png` → `text/plain` (explicit wins), `gm47.eml` → `application/octet-stream`, `gm47.txt` → `text/plain`. No `error`. A probe in the same round sent an explicit `mime_type` with one empty half: `"application/"` and `"/pdf"` passed `_attachment_part`'s malformed-type check, and Gmail stored them verbatim as the attachments' `mime_type` (`application/`, `/pdf`). Before #896 these became `application/octet-stream` and `application/pdf`. An explicit `message/rfc822` `fwd.eml` with an 8-bit UTF-8 body came back as `message/rfc822` (115 bytes). Both messages were trashed.
|
|
592
|
+
|
|
593
|
+
**Result (2026-10-07, PR #928 round 3) ✅ PASS**: via `mcp-gee-sweet-kit`, after reconnecting to `542f548`. All 6 attachments came back with the same `mime_type`s as round 2, with no `error`. Re-running round 2's probe, `send_message` with an explicit `mime_type` of `"application/"` and then `"/pdf"` each returned `{"error": "Attachment mime_type '...' isn't a type/subtype such as 'application/pdf'."}` and no `id`. A `list_messages` search with `include_spam_trash: true` found nothing sent. The TC-GM47 message was trashed.
|
|
594
|
+
|
|
595
|
+
**Result (2026-10-07, PR #928 after the `develop` merge `548e691`) ✅ PASS**: via `mcp-gee-sweet-kit`, after reconnecting to the merge, which brought in #925's OAuth re-authorization. All 6 attachments came back with the same `mime_type`s as before, with no `error`. The message was trashed.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
557
599
|
## `create_draft`
|
|
558
600
|
|
|
559
601
|
### TC-GM13: Create a draft ⚠️ destructive
|
|
@@ -578,6 +620,8 @@ Uses a throwaway, not a fixture: removing `mcp-qa-fixture` from a fixture would
|
|
|
578
620
|
**Checks**
|
|
579
621
|
- Returns `{"error": "..."}` that mentions `local_path` and `content_base64`, and no draft `id`
|
|
580
622
|
|
|
623
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS (regression, error now raised inside `_compose_raw`'s thread)**: returned `{"error": "Each attachment needs either local_path or content_base64 (plus optional filename and mime_type)."}`, with no draft `id`.
|
|
624
|
+
|
|
581
625
|
---
|
|
582
626
|
|
|
583
627
|
### TC-GM41: Draft with HTML and an attachment survives `send_draft` ⚠️ destructive
|
|
@@ -596,6 +640,8 @@ Uses a throwaway, not a fixture: removing `mcp-qa-fixture` from a fixture would
|
|
|
596
640
|
|
|
597
641
|
**Cleanup:** `trash_message` the step-3 `id`.
|
|
598
642
|
|
|
643
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS (regression, `create_draft`/`send_draft` via `_compose_raw` and `_message_summary`)**: step 2 had one attachment, `gm41.txt` (`text/plain`, 18 bytes), and `label_ids` `DRAFT`. `send_draft` returned `id`, `thread_id`, and `label_ids` (`SENT`, no `DRAFT`). Step 4's bodies and attachment `filename`/`mime_type`/`size` equal step 2's. Trashed.
|
|
644
|
+
|
|
599
645
|
---
|
|
600
646
|
|
|
601
647
|
## `send_draft`
|
|
@@ -640,12 +686,19 @@ Uses a throwaway, not a fixture: removing `mcp-qa-fixture` from a fixture would
|
|
|
640
686
|
|
|
641
687
|
**Checks**
|
|
642
688
|
- Step 3: `thread_id` equals `{GM17_ORIG}`'s `thread_id`
|
|
689
|
+
- Step 3: no `warning` field. One appears only when the mailbox's own addresses couldn't be looked up (#802), which a healthy token never hits; the failure path is unit-tested only.
|
|
643
690
|
- Step 4: `headers.in_reply_to` equals `{GM17_MID}`, and `headers.references` ends with `{GM17_MID}`
|
|
644
691
|
- Step 4: `headers.subject` is exactly `Re: [mcp-qa:tc-gm17] reply original` (one `Re: `)
|
|
645
692
|
- Step 4: `headers.to` is `{QA+tc-gm17}` (a reply to your own sent message goes to its original `To`, per TC-GM24)
|
|
646
693
|
|
|
647
694
|
**Cleanup:** `trash_message` `{GM17_ORIG}` and the step-3 `id`.
|
|
648
695
|
|
|
696
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS**: via `mcp-gee-sweet-kit`. Step 3's `thread_id` equals the original's, and there was no `warning` field. Step 4: `in_reply_to` and `references` are both the original's Message-ID, the subject is `Re: [mcp-qa:tc-gm17] reply original` (one `Re: `), and `to` is the `+tc-gm17` address. Both messages were trashed.
|
|
697
|
+
|
|
698
|
+
**Result (2026-10-07, PR #928 round 2) ✅ PASS**: via `mcp-gee-sweet-kit` at `1d3c7cf`, run right after `refresh_cache()` with no arguments (which now also clears the cached own-address set), so the reply re-read the send-as aliases. `refresh_cache` returned `{"invalidated": "all"}`. Step 3's `thread_id` equals the original's, and there was no `warning` field. Step 4: `in_reply_to` and `references` are the original's Message-ID, the subject is `Re: [mcp-qa:tc-gm17] reply original`, and `to` is the `+tc-gm17` address. Both messages were trashed.
|
|
699
|
+
|
|
700
|
+
**Result (2026-10-07, PR #928 after the `develop` merge `548e691`) ✅ PASS**: via `mcp-gee-sweet-kit`, run right after `refresh_cache()` (`{"invalidated": "all"}`) on the merged code. The reply has the original's `thread_id` and no `warning`. `in_reply_to` and `references` are the original's Message-ID, the subject is `Re: [mcp-qa:tc-gm17] reply original`, and `to` is the `+tc-gm17` address. Both messages were trashed.
|
|
701
|
+
|
|
649
702
|
---
|
|
650
703
|
|
|
651
704
|
### TC-GM18: Reply to non-existent message
|
|
@@ -810,6 +863,8 @@ Run after TC-GM33. The middle message (M2) was sent by the QA mailbox, so the re
|
|
|
810
863
|
|
|
811
864
|
**Cleanup:** if step 1 showed `UNREAD`, add it back with `modify_labels` so the fixture ends as it started.
|
|
812
865
|
|
|
866
|
+
**Result (2026-10-06, PR #928 round 1) ✅ PASS (regression, `modify_labels` message branch via `_message_summary`)**: the message branch returned `id`, `thread_id`, and `label_ids`. `UNREAD` was added, then removed. On the thread, `STARRED` was added to every message, then removed from every message. There was no `error` in any response. The fixture started `UNREAD`, and `UNREAD` was restored.
|
|
867
|
+
|
|
813
868
|
---
|
|
814
869
|
|
|
815
870
|
### TC-GM20: Validation errors
|