mcp-gee-sweet 0.9.2.dev283__tar.gz → 0.9.2.dev286__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.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/amy.md +3 -1
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_content.md +56 -1
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/roadmap.md +1 -1
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/content.py +73 -64
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/emitter.py +8 -4
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/html_parser.py +6 -3
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/images.py +28 -11
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_content.py +80 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_core.py +76 -42
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_images.py +39 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_tables.py +13 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/notify-kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/notify-partner.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/qa-kickoff.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/roadmap-status.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/bob.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/joy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.dockerignore +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.gitignore +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.python-version +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CLAUDE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/Dockerfile +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/LICENSE +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/Makefile +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/assets/logo.png +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-comments-first-class.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-gmail-domain.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-local-fs-test-harness.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-pillow-image-dependency.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-prompt-qa-role.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-tool-error-visibility.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/role-file-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/blockquote-representation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/borderless-table-columns.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/doc-to-markdown.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/heading-anchor-resolution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/module-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/sse-consent-off-event-loop.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/gmail-test-plan.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-09-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/retro-v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_layout.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_style.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_tables.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_activity.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_files.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_sharing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_transfer.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/gmail.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/style-guide.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/qa_cancel_insert_local_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/write_team_mcp_configs.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/server.json +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/http_transport.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/reauth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/pagination.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/fakes.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/integration/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/integration/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/integration/test_local_fs_harness.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_reauth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_server_json.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_team_mcp_configs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/uv.lock +0 -0
|
@@ -2,7 +2,9 @@ Tech Writer (Amy). Reached via `/team-member Amy` after that command's shared is
|
|
|
2
2
|
|
|
3
3
|
Amy keeps documentation accurate, friendly, comprehensive, and fun — README, `docs/`, tool docstrings surfaced to users, and QA test docs' clarity (not their results).
|
|
4
4
|
|
|
5
|
-
- **On `team/amy` (idle):** claim work the same way Ash/Jay do, but from the `documentation` label instead of `ready-for-development`: `gh issue list --repo khuisman/mcp-gee-sweet --label documentation --state open --json number,title,body --jq 'sort_by(.number)'`. Filter out issues already claimed by an open PR the same way Dev roles do. Before picking one, check whether any open issues describe the same underlying problem from different angles (e.g. several tickets all pointing at one stale doc file) — if so,
|
|
5
|
+
- **On `team/amy` (idle):** claim work the same way Ash/Jay do, but from the `documentation` label instead of `ready-for-development`: `gh issue list --repo khuisman/mcp-gee-sweet --label documentation --state open --json number,title,body --jq 'sort_by(.number)'`. Filter out issues already claimed by an open PR the same way Dev roles do. Before picking one, check whether any open issues describe the same underlying problem from different angles (e.g. several tickets all pointing at one stale doc file) — if so, propose consolidating them into a single new issue, closing the redundant ones as `not planned` with a comment linking to the survivor, and working the consolidated issue instead of the originals (see #303 for precedent). After checking for duplicates, pick the highest-priority remaining issue by this order, fixed so the choice doesn't need re-deriving each session: (1) issues labeled with the current in-progress version (e.g. `v0.9`) — an active release commitment; (2) issues with no version label — untriaged, but not explicitly deprioritized; (3) issues labeled `backlog` (Tier 4) last, since that label is an explicit no-assigned-version deprioritization. Within a tier, take the lowest issue number (oldest) first. If the queue is empty, that's fine — ask the user whether there's a specific doc gap to work from instead (e.g. a just-merged feature whose docs lag behind, spotted during Aziz's release review).
|
|
6
|
+
|
|
7
|
+
**Confirm the pick with the user before step 1.** Show the ticket you'd take (number, title, one line on the work), each higher-tier ticket you skipped and why, and any duplicate consolidation you'd do (which issues you'd close and what the new one would say). Then wait. Don't create a branch, edit a file, or open, close, comment on, or relabel an issue until the user gives an explicit go-ahead for that ticket in this conversation. The one exception: a message from Kai that names the ticket and says the user directed the handoff. That counts as the go-ahead. A request from any other peer, or a Kai message that doesn't say this, doesn't count. Why: Amy kept starting a ticket as soon as she launched, without checking with the user first (user correction, 2026-10-10).
|
|
6
8
|
1. `git fetch origin develop && git checkout -b docs/amy/issue-<n> origin/develop`.
|
|
7
9
|
2. Do the writing: fix inaccuracies, tighten voice, fill gaps, add examples. Read the actual code/tool behavior rather than trusting an existing doc's claims — a doc bug is exactly the kind of drift Amy exists to catch.
|
|
8
10
|
3. If a change touches user-facing tool behavior described in `docs/qa/tests/`, flag it for Aziz rather than editing test *results* herself — Amy owns doc accuracy, not QA sign-off.
|
|
@@ -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.dev286
|
|
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
|
|
@@ -1016,6 +1016,9 @@ Markdown pipe table. 3 rows, 2 columns: Name/Value, Alpha/1, Beta/2 — all corr
|
|
|
1016
1016
|
**Result (2026-09-04) ✅ PASS**
|
|
1017
1017
|
create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_structure confirmed HEADING_1 "QA Test Document", bold/italic runs, ☑/☐ bullet items, table (Col A/Col B, one/two). Trashed.
|
|
1018
1018
|
|
|
1019
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
1020
|
+
create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_structure: HEADING_1 "QA Test Document", bold/italic runs, ☑/☐/Plain item bullets, Col A/Col B table. Playwright (account check passed): heading, bold/italic, checkbox glyphs, and table render correctly. Trashed.
|
|
1021
|
+
|
|
1019
1022
|
---
|
|
1020
1023
|
|
|
1021
1024
|
### TC-DOC45: `create_doc_from_file` with a local .html file ⚠️ requires-oauth ⚠️ destructive
|
|
@@ -1036,6 +1039,9 @@ create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_s
|
|
|
1036
1039
|
**Result (2026-09-04) ✅ PASS**
|
|
1037
1040
|
create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc_structure confirmed HEADING_2 "From HTML file", paragraph "Content paragraph." Trashed.
|
|
1038
1041
|
|
|
1042
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
1043
|
+
create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph." Trashed.
|
|
1044
|
+
|
|
1039
1045
|
---
|
|
1040
1046
|
|
|
1041
1047
|
### TC-DOC46: `create_doc_from_file` file not found
|
|
@@ -1051,6 +1057,9 @@ create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc
|
|
|
1051
1057
|
**Result (2026-09-04) ✅ PASS**
|
|
1052
1058
|
create_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception.
|
|
1053
1059
|
|
|
1060
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
1061
|
+
create_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception. Unsupported .txt also still returns the no-hint message "Use .md or .html/.htm" (shared helper, format_overridable=False).
|
|
1062
|
+
|
|
1054
1063
|
---
|
|
1055
1064
|
|
|
1056
1065
|
### TC-DOC47: `write_doc_content` inline code monospace ⚠️ destructive
|
|
@@ -2804,6 +2813,9 @@ insert_local_images (default revoke_sharing) on scratch doc with "Marker: IMGMAR
|
|
|
2804
2813
|
**Result (2026-09-04) ✅ PASS**
|
|
2805
2814
|
create_doc HTML table with image in a cell: no error. get_doc_structure: cell 0 "Before", cell 2 "After", cell 1 (image) empty (""). No images key in response (image dropped at parse time, table-cell images still out of scope). No timeout/hang. Trashed.
|
|
2806
2815
|
|
|
2816
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
2817
|
+
create_doc HTML table with an image in a cell: returned at once, no error, no `images` key. get_doc_structure: cell 0 "Before", cell 1 "", cell 2 "After". No hang. Trashed.
|
|
2818
|
+
|
|
2807
2819
|
---
|
|
2808
2820
|
|
|
2809
2821
|
### TC-DOC154: A `<pre>` block resumed after a nested table doesn't leave a spurious trailing whitespace-only paragraph ⚠️ requires-oauth ⚠️ destructive
|
|
@@ -3107,6 +3119,9 @@ Generated real ~76MB 4800x5000 (24.0MP, under 25MP pre-check) noise PNG. Main ca
|
|
|
3107
3119
|
**Result (2026-09-04) ✅ PASS**
|
|
3108
3120
|
update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: returned docId matching DOC_ID, no error. get_doc_structure matches TC-DOC44's create_doc_from_file output exactly (HEADING_1, bold/italic runs, ☑/☐ items, Col A/Col B table). get_file_metadata before/after identical name/parents/webViewLink — doc identity/location preserved.
|
|
3109
3121
|
|
|
3122
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
3123
|
+
update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: docId matched, web_link present, no error. get_doc_structure matches TC-DOC44's output (HEADING_1, bold/italic, ☑/☐ items, Col A/Col B table). get_file_metadata before/after: same name/parents/webViewLink.
|
|
3124
|
+
|
|
3110
3125
|
### TC-DOC169: `update_doc_from_file` with a local .html file ⚠️ destructive
|
|
3111
3126
|
**Prompt**
|
|
3112
3127
|
> "Update Google Doc {DOC_ID} from the file <repo-root>/docs/qa/fixtures/tc-d196-create-doc.html"
|
|
@@ -3123,6 +3138,9 @@ update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: returned docId matching D
|
|
|
3123
3138
|
**Result (2026-09-04) ✅ PASS**
|
|
3124
3139
|
update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph." — matches TC-DOC45.
|
|
3125
3140
|
|
|
3141
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
3142
|
+
update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph."
|
|
3143
|
+
|
|
3126
3144
|
### TC-DOC170: `update_doc_from_file` file not found
|
|
3127
3145
|
**Prompt**
|
|
3128
3146
|
> "Update Google Doc {DOC_ID} from the file ~/does-not-exist.md"
|
|
@@ -3137,6 +3155,9 @@ update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_
|
|
|
3137
3155
|
**Result (2026-09-04) ✅ PASS**
|
|
3138
3156
|
update_doc_from_file('~/does-not-exist.md') returned {"error":"File not found: ~/does-not-exist.md"}, no exception. get_doc_content confirmed doc unchanged (still HEADING_2/paragraph from DOC169).
|
|
3139
3157
|
|
|
3158
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
3159
|
+
update_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception. get_doc_content unchanged.
|
|
3160
|
+
|
|
3140
3161
|
### TC-DOC171: `update_doc_from_file` unsupported extension with no override returns an error, doc untouched
|
|
3141
3162
|
**Setup:** create a local file `qa-update.txt` with arbitrary plain text
|
|
3142
3163
|
|
|
@@ -3155,6 +3176,9 @@ update_doc_from_file('~/does-not-exist.md') returned {"error":"File not found: ~
|
|
|
3155
3176
|
**Result (2026-09-04) ✅ PASS**
|
|
3156
3177
|
update_doc_from_file(qa-update.txt, no override) returned {"error":"Unsupported file extension '.txt'. Use .md or .html/.htm, or pass content_format explicitly."}. get_doc_content confirmed doc untouched — no Docs API call made.
|
|
3157
3178
|
|
|
3179
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
3180
|
+
update_doc_from_file(qa-update.txt) returned {"error": "Unsupported file extension '.txt'. Use .md or .html/.htm, or pass content_format explicitly."}. get_doc_content unchanged.
|
|
3181
|
+
|
|
3158
3182
|
### TC-DOC172: `update_doc_from_file` `content_format` explicitly overrides an unrecognized extension ⚠️ destructive
|
|
3159
3183
|
**Setup:** create a local file `qa-update-override.txt` containing `# Overridden Heading\n\nParagraph text.\n`
|
|
3160
3184
|
|
|
@@ -3171,6 +3195,9 @@ update_doc_from_file(qa-update.txt, no override) returned {"error":"Unsupported
|
|
|
3171
3195
|
**Result (2026-09-04) ✅ PASS**
|
|
3172
3196
|
update_doc_from_file(qa-update-override.txt, content_format='markdown') succeeded, no error. get_doc_structure: HEADING_1 "Overridden Heading", paragraph "Paragraph text." Fixture restored after.
|
|
3173
3197
|
|
|
3198
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
3199
|
+
update_doc_from_file(qa-update-override.txt, content_format='markdown'): no error. get_doc_structure: HEADING_1 "Overridden Heading", paragraph "Paragraph text." Fixture restored to setup.md's seed HTML via write_doc_content afterward.
|
|
3200
|
+
|
|
3174
3201
|
---
|
|
3175
3202
|
|
|
3176
3203
|
## `get_doc_as_markdown`
|
|
@@ -3747,12 +3774,15 @@ Tool call: `insert_local_images(doc_id={DOC_ID}, images=[{"marker": "IMGMARKERBO
|
|
|
3747
3774
|
**Checks**
|
|
3748
3775
|
- `results[0]` has no `fileId` and an `error` containing "14000x13000 (182.0 megapixels)" and "25 megapixels"
|
|
3749
3776
|
- No `qa-bomb.png` appears in `{FOLDER_ID}`, and the marker text is unchanged
|
|
3750
|
-
- Re-run with `auto_downscale=True`: `results[0]` still has no `fileId`, and its `error` ends with "Could not auto-downscale it (unreadable or corrupt image, animated,
|
|
3777
|
+
- Re-run with `auto_downscale=True`: `results[0]` still has no `fileId`, and its `error` ends with "Could not auto-downscale it (unreadable or corrupt image, animated, too large to decode safely, or still over the file-size limit after shrinking)." (the last reason was added by #894/#581)
|
|
3751
3778
|
|
|
3752
3779
|
**Cleanup:** trash the doc; `rm /tmp/qa-bomb.png`
|
|
3753
3780
|
|
|
3754
3781
|
**Result (2026-09-24) ✅ PASS — Kit, PR #801 round 2 (fix `4d5f842`, issue #560).** Both checks matched exactly, and nothing was uploaded. (Round 1 reproduced the pre-fix behavior locally: `check_image_file` returned `None` for this file.) Doc trashed.
|
|
3755
3782
|
|
|
3783
|
+
**Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
|
|
3784
|
+
insert_local_images with the 14000x13000 PNG: results[0] no fileId, error names "14000x13000 (182.0 megapixels)" and "25 megapixels". With auto_downscale=True: error ends with "Could not auto-downscale it (unreadable or corrupt image, animated, too large to decode safely, or still over the file-size limit after shrinking)." No qa-bomb.png in FOLDER_ID; marker text unchanged. Doc trashed.
|
|
3785
|
+
|
|
3756
3786
|
---
|
|
3757
3787
|
|
|
3758
3788
|
### TC-DOC198: `insert_local_images` — an unreadable local file fails with "failed to read local file", not a raw upload error (PR #801)
|
|
@@ -3935,3 +3965,28 @@ Two extra probes ran from a scratch copy of the script (not committed):
|
|
|
3935
3965
|
All files from both probes and the three steps, and the doc, were trashed.
|
|
3936
3966
|
|
|
3937
3967
|
**Result (2026-10-04) ✅ PASS — Kit, PR #903 round 2 (`4e392c3`).** All six steps ran from the kit worktree with its OAuth token, on a fresh scratch doc in `{FOLDER_ID}`. Each step printed `cancelled at '<point>'; the tool returned nothing` and the WARNING `Doc edit for {DOC_ID} failed or was cancelled after its images were resolved; files created by this call: <one file ID>; pre-existing files: none; still link-shared by this call: ...`. That last field listed `{KEPT_FILE_ID}` for step 4 and was `none` for every other step. Steps 2 (`share-fail`, the orphan) and 6 (`revoke`), which logged nothing in round 1's probe, now log. `list_permissions` showed no `anyone` grant on `{SHARE_FILE_ID}`, `{ORPHAN_FILE_ID}`, `{EDIT_FILE_ID}`, `{INFLIGHT_FILE_ID}`, or `{REVOKE_FILE_ID}`, and showed `anyoneWithLink`/`reader` on `{KEPT_FILE_ID}`. After `refresh_cache`, `get_doc_as_markdown` read `Before`, `IMGMARKER883`, an image, an image, `After`. So steps 1–4 left `IMGMARKER883` untouched, step 6's edit landed before its cancel, and step 5's in-flight edit also landed (an image replaced `INFLIGHT883`). All six files and the doc were trashed.
|
|
3968
|
+
|
|
3969
|
+
## Unreadable source file returns an error — `create_doc_from_file` / `update_doc_from_file` (PR #961)
|
|
3970
|
+
|
|
3971
|
+
Both tools read the local file through the shared `_read_doc_source_file` helper. Before PR #961's round 2, a path that existed but wasn't a readable UTF-8 regular file raised out of the tool (`[Errno 21] Is a directory`, `'utf-8' codec can't decode byte 0xe9 ...`) instead of returning `{"error": ...}`.
|
|
3972
|
+
|
|
3973
|
+
### TC-DOC208: A directory or non-UTF-8 source file returns `{"error": ...}`, and nothing is created or edited
|
|
3974
|
+
**Setup:**
|
|
3975
|
+
- `mkdir -p /tmp/tc-doc208/dir.md`
|
|
3976
|
+
- `python3 -c "open('/tmp/tc-doc208/latin1.html','wb').write('<p>caf\xe9</p>'.encode('latin-1'))"`
|
|
3977
|
+
- `get_doc_content(file_id={DOC_ID})`, note the content for comparison afterward
|
|
3978
|
+
|
|
3979
|
+
**Steps**
|
|
3980
|
+
1. `create_doc_from_file(local_path="/tmp/tc-doc208/dir.md", folder_id={FOLDER_ID})`
|
|
3981
|
+
2. `create_doc_from_file(local_path="/tmp/tc-doc208/latin1.html", folder_id={FOLDER_ID})`
|
|
3982
|
+
3. `update_doc_from_file(doc_id={DOC_ID}, local_path="/tmp/tc-doc208/dir.md")`
|
|
3983
|
+
4. `update_doc_from_file(doc_id={DOC_ID}, local_path="/tmp/tc-doc208/latin1.html")`
|
|
3984
|
+
|
|
3985
|
+
**Checks**
|
|
3986
|
+
- Each call returns a dict with an `error` key naming the path (any wording). None comes back as `Error executing tool ...`
|
|
3987
|
+
- `list_files(folder_id={FOLDER_ID})` shows no new Doc named `dir` or `latin1` (the filename stem `create_doc_from_file` uses as the default title; steps 1–2 fail before the Drive `create`)
|
|
3988
|
+
- `get_doc_content(file_id={DOC_ID})` matches the Setup snapshot (steps 3–4 fail before any Docs API call)
|
|
3989
|
+
|
|
3990
|
+
**Cleanup:** `rm -rf /tmp/tc-doc208`
|
|
3991
|
+
|
|
3992
|
+
**Result (2026-10-10) ✅ PASS — Sky, PR #961 round 2 (`849f0bb`).** Ran with the fixtures under this job's scratch dir instead of `/tmp/tc-doc208`. Steps 1 and 3 returned `{"error": "Not a regular file: <path>/dir.md"}`; steps 2 and 4 returned `{"error": "File is not valid UTF-8: <path>/latin1.html (invalid continuation byte at byte 6)"}`. No call surfaced as `Error executing tool`. `list_files({FOLDER_ID})` showed only the three standing fixtures. After `refresh_cache`, `get_doc_content({DOC_ID})` matched the Setup snapshot. Round 1 reproduced both raises on the pre-fix code (`075ce99`).
|
|
@@ -351,7 +351,7 @@ Requires `gmail/v1` client and `https://www.googleapis.com/auth/gmail.modify` sc
|
|
|
351
351
|
- [ ] Define the correctness rubric and own execution for the composite-vs-primitive efficiency benchmark — Aziz ([#584](https://github.com/khuisman/mcp-gee-sweet/issues/584))
|
|
352
352
|
- [ ] QA `.env` has no single source of truth — `setup_team.sh` only copies `.env` into a worktree when absent, so fixture-ID changes never propagate (had to hand-sync 9 copies for #305); also `run.md` vs `setup.md` disagree on `docs/qa/.env` vs repo-root `.env`. Surfaced by PR #677, relates to [#648](https://github.com/khuisman/mcp-gee-sweet/issues/648) ([#679](https://github.com/khuisman/mcp-gee-sweet/issues/679))
|
|
353
353
|
- [x] `list_files`/`get_file_metadata` docstrings cite a stale `upload_local_file` `modifiedTime` caveat (fixed at the root by PR #472) and carry a broken self-referential contrast — from Bob's `docs/tools.md` docstring sweep; Dev/QA track, batches with #682/#683 ([#681](https://github.com/khuisman/mcp-gee-sweet/issues/681)) (PR 898)
|
|
354
|
-
- [
|
|
354
|
+
- [x] `create_doc` family docstrings name only the ~25MP inline-image limit; the ~50MB byte-size limit (#562) is equally enforced, and 3 deferring docstrings inherit the gap — from Bob's `docs/tools.md` docstring sweep. Routed to Amy (`documentation`); closed community PR #932 has a diff ([#682](https://github.com/khuisman/mcp-gee-sweet/issues/682)) (PR 970)
|
|
355
355
|
- [ ] `qa(docs)`: no comprehensive fixture exercising every Docs formatting construct in one pass — Aziz, triaged out of `backlog` 2026-09-11 ([#477](https://github.com/khuisman/mcp-gee-sweet/issues/477))
|
|
356
356
|
- [ ] Fact-check and word competitive/benchmark claims once real benchmark numbers exist — routed to Amy, triaged out of `backlog` 2026-09-11 ([#585](https://github.com/khuisman/mcp-gee-sweet/issues/585))
|
|
357
357
|
- [x] QA test-doc cross-reference cleanup after the `drive.md`/`docs.md` split (PR #582) — triaged out of `backlog` 2026-09-11 ([#590](https://github.com/khuisman/mcp-gee-sweet/issues/590)) (PR #851)
|
{mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/content.py
RENAMED
|
@@ -13,7 +13,7 @@ from markdown.inlinepatterns import InlineProcessor
|
|
|
13
13
|
from mcp.server.mcpserver import Context
|
|
14
14
|
from mcp.types import ToolAnnotations
|
|
15
15
|
|
|
16
|
-
from ...auth import execute_in_thread
|
|
16
|
+
from ...auth import SpreadsheetContext, execute_in_thread
|
|
17
17
|
from ...cache import CACHE_VALIDATE_MODIFIED_TIME
|
|
18
18
|
from ..drive import _SA_QUOTA_ERROR
|
|
19
19
|
from ..drive.transfer import _GOOGLE_DOC_MIME
|
|
@@ -266,14 +266,45 @@ async def _resolve_image_source(
|
|
|
266
266
|
)
|
|
267
267
|
|
|
268
268
|
|
|
269
|
+
_SOURCE_FILE_FORMATS = {".md": "markdown", ".html": "html", ".htm": "html"}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _read_doc_source_file(
|
|
273
|
+
local_path: str, content_format: str | None, *, format_overridable: bool
|
|
274
|
+
) -> tuple[str, str] | dict[str, Any]:
|
|
275
|
+
"""Shared file reading for create_doc_from_file / update_doc_from_file (#567):
|
|
276
|
+
returns (content, content_format), or {"error": ...} for a missing file, a path
|
|
277
|
+
that isn't a regular file, an unreadable or non-UTF-8 file, or an extension with
|
|
278
|
+
no inferable format. content_format=None infers it from the extension.
|
|
279
|
+
format_overridable says whether the calling tool takes a content_format
|
|
280
|
+
parameter, so the error only suggests one when it exists."""
|
|
281
|
+
path = Path(local_path)
|
|
282
|
+
if not path.exists():
|
|
283
|
+
return {"error": f"File not found: {local_path}"}
|
|
284
|
+
if not path.is_file():
|
|
285
|
+
return {"error": f"Not a regular file: {local_path}"}
|
|
286
|
+
if content_format is None:
|
|
287
|
+
ext = path.suffix.lower()
|
|
288
|
+
inferred = _SOURCE_FILE_FORMATS.get(ext)
|
|
289
|
+
if inferred is None:
|
|
290
|
+
hint = ", or pass content_format explicitly." if format_overridable else ""
|
|
291
|
+
return {"error": f"Unsupported file extension '{ext}'. Use .md or .html/.htm{hint}"}
|
|
292
|
+
content_format = inferred
|
|
293
|
+
try:
|
|
294
|
+
content = path.read_text(encoding="utf-8")
|
|
295
|
+
except UnicodeDecodeError as e:
|
|
296
|
+
return {"error": f"File is not valid UTF-8: {local_path} ({e.reason} at byte {e.start})"}
|
|
297
|
+
except OSError as e:
|
|
298
|
+
return {"error": f"Could not read file {local_path}: {e.strerror or e}"}
|
|
299
|
+
return content, content_format
|
|
300
|
+
|
|
301
|
+
|
|
269
302
|
async def _replace_doc_content(
|
|
270
|
-
|
|
271
|
-
drive_service,
|
|
272
|
-
doc_cache,
|
|
273
|
-
folder_id: str | None,
|
|
303
|
+
lc: SpreadsheetContext,
|
|
274
304
|
doc_id: str,
|
|
275
305
|
content: str,
|
|
276
306
|
content_format: str,
|
|
307
|
+
*,
|
|
277
308
|
autolink_urls: bool,
|
|
278
309
|
revoke_sharing: bool,
|
|
279
310
|
auto_downscale: bool,
|
|
@@ -283,7 +314,14 @@ async def _replace_doc_content(
|
|
|
283
314
|
the clear-then-insert mechanism (including the same-batch insertText
|
|
284
315
|
style-inheritance quirk below) has exactly one implementation instead of two
|
|
285
316
|
copies that could drift out of sync.
|
|
317
|
+
|
|
318
|
+
The three flags are keyword-only (#567), here and in _apply_doc_content: as
|
|
319
|
+
positional bools, swapping two at a call site would type-check and silently do
|
|
320
|
+
the wrong thing.
|
|
286
321
|
"""
|
|
322
|
+
docs_service = lc.docs_service
|
|
323
|
+
drive_service = lc.drive_service
|
|
324
|
+
doc_cache = lc.doc_cache
|
|
287
325
|
doc = await execute_in_thread(
|
|
288
326
|
docs_service.documents().get(documentId=doc_id).execute,
|
|
289
327
|
docs_service,
|
|
@@ -330,10 +368,10 @@ async def _replace_doc_content(
|
|
|
330
368
|
doc_id,
|
|
331
369
|
content,
|
|
332
370
|
content_format,
|
|
333
|
-
autolink_urls,
|
|
334
|
-
folder_id,
|
|
335
|
-
revoke_sharing,
|
|
336
|
-
auto_downscale,
|
|
371
|
+
autolink_urls=autolink_urls,
|
|
372
|
+
target_folder_id=lc.folder_id,
|
|
373
|
+
revoke_sharing=revoke_sharing,
|
|
374
|
+
auto_downscale=auto_downscale,
|
|
337
375
|
)
|
|
338
376
|
except DocEditError as e:
|
|
339
377
|
# The clear above already committed, so the doc changed either way.
|
|
@@ -382,6 +420,7 @@ async def _apply_doc_content(
|
|
|
382
420
|
doc_id: str,
|
|
383
421
|
content: str,
|
|
384
422
|
content_format: str,
|
|
423
|
+
*,
|
|
385
424
|
autolink_urls: bool,
|
|
386
425
|
target_folder_id: str | None,
|
|
387
426
|
revoke_sharing: bool,
|
|
@@ -932,10 +971,10 @@ def register(tool):
|
|
|
932
971
|
doc_id,
|
|
933
972
|
content,
|
|
934
973
|
content_format,
|
|
935
|
-
autolink_urls,
|
|
936
|
-
target_folder_id,
|
|
937
|
-
revoke_sharing,
|
|
938
|
-
auto_downscale,
|
|
974
|
+
autolink_urls=autolink_urls,
|
|
975
|
+
target_folder_id=target_folder_id,
|
|
976
|
+
revoke_sharing=revoke_sharing,
|
|
977
|
+
auto_downscale=auto_downscale,
|
|
939
978
|
)
|
|
940
979
|
except DocEditError as e:
|
|
941
980
|
if target_folder_id:
|
|
@@ -1010,20 +1049,11 @@ def register(tool):
|
|
|
1010
1049
|
Requires OAuth or ADC auth. Service accounts cannot create files in personal
|
|
1011
1050
|
Drive (no storage quota). Check server://auth-status for your current auth method.
|
|
1012
1051
|
"""
|
|
1013
|
-
|
|
1014
|
-
if
|
|
1015
|
-
return
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if ext == ".md":
|
|
1019
|
-
content_format = "markdown"
|
|
1020
|
-
elif ext in (".html", ".htm"):
|
|
1021
|
-
content_format = "html"
|
|
1022
|
-
else:
|
|
1023
|
-
return {"error": f"Unsupported file extension '{ext}'. Use .md or .html/.htm"}
|
|
1024
|
-
|
|
1025
|
-
content = path.read_text(encoding="utf-8")
|
|
1026
|
-
doc_title = title or path.stem
|
|
1052
|
+
source = _read_doc_source_file(local_path, None, format_overridable=False)
|
|
1053
|
+
if isinstance(source, dict):
|
|
1054
|
+
return source
|
|
1055
|
+
content, content_format = source
|
|
1056
|
+
doc_title = title or Path(local_path).stem
|
|
1027
1057
|
|
|
1028
1058
|
lc = ctx.request_context.lifespan_context
|
|
1029
1059
|
drive_service = lc.drive_service
|
|
@@ -1066,10 +1096,10 @@ def register(tool):
|
|
|
1066
1096
|
doc_id,
|
|
1067
1097
|
content,
|
|
1068
1098
|
content_format,
|
|
1069
|
-
autolink_urls,
|
|
1070
|
-
target_folder_id,
|
|
1071
|
-
revoke_sharing,
|
|
1072
|
-
auto_downscale,
|
|
1099
|
+
autolink_urls=autolink_urls,
|
|
1100
|
+
target_folder_id=target_folder_id,
|
|
1101
|
+
revoke_sharing=revoke_sharing,
|
|
1102
|
+
auto_downscale=auto_downscale,
|
|
1073
1103
|
)
|
|
1074
1104
|
except DocEditError as e:
|
|
1075
1105
|
if target_folder_id:
|
|
@@ -1222,16 +1252,13 @@ def register(tool):
|
|
|
1222
1252
|
"""
|
|
1223
1253
|
lc = ctx.request_context.lifespan_context
|
|
1224
1254
|
return await _replace_doc_content(
|
|
1225
|
-
lc
|
|
1226
|
-
lc.drive_service,
|
|
1227
|
-
lc.doc_cache,
|
|
1228
|
-
lc.folder_id,
|
|
1255
|
+
lc,
|
|
1229
1256
|
doc_id,
|
|
1230
1257
|
content,
|
|
1231
1258
|
content_format,
|
|
1232
|
-
autolink_urls,
|
|
1233
|
-
revoke_sharing,
|
|
1234
|
-
auto_downscale,
|
|
1259
|
+
autolink_urls=autolink_urls,
|
|
1260
|
+
revoke_sharing=revoke_sharing,
|
|
1261
|
+
auto_downscale=auto_downscale,
|
|
1235
1262
|
)
|
|
1236
1263
|
|
|
1237
1264
|
@tool(annotations=ToolAnnotations(title="Update Document from File", destructiveHint=True))
|
|
@@ -1284,38 +1311,20 @@ def register(tool):
|
|
|
1284
1311
|
instead, with any temporary image share already revoked. The old content
|
|
1285
1312
|
was cleared first, so the doc may be empty or partly written.
|
|
1286
1313
|
"""
|
|
1287
|
-
|
|
1288
|
-
if
|
|
1289
|
-
return
|
|
1290
|
-
|
|
1291
|
-
if content_format is None:
|
|
1292
|
-
ext = path.suffix.lower()
|
|
1293
|
-
if ext == ".md":
|
|
1294
|
-
content_format = "markdown"
|
|
1295
|
-
elif ext in (".html", ".htm"):
|
|
1296
|
-
content_format = "html"
|
|
1297
|
-
else:
|
|
1298
|
-
return {
|
|
1299
|
-
"error": (
|
|
1300
|
-
f"Unsupported file extension '{ext}'. Use .md or .html/.htm, "
|
|
1301
|
-
"or pass content_format explicitly."
|
|
1302
|
-
)
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
content = path.read_text(encoding="utf-8")
|
|
1314
|
+
source = _read_doc_source_file(local_path, content_format, format_overridable=True)
|
|
1315
|
+
if isinstance(source, dict):
|
|
1316
|
+
return source
|
|
1317
|
+
content, content_format = source
|
|
1306
1318
|
|
|
1307
1319
|
lc = ctx.request_context.lifespan_context
|
|
1308
1320
|
return await _replace_doc_content(
|
|
1309
|
-
lc
|
|
1310
|
-
lc.drive_service,
|
|
1311
|
-
lc.doc_cache,
|
|
1312
|
-
lc.folder_id,
|
|
1321
|
+
lc,
|
|
1313
1322
|
doc_id,
|
|
1314
1323
|
content,
|
|
1315
1324
|
content_format,
|
|
1316
|
-
autolink_urls,
|
|
1317
|
-
revoke_sharing,
|
|
1318
|
-
auto_downscale,
|
|
1325
|
+
autolink_urls=autolink_urls,
|
|
1326
|
+
revoke_sharing=revoke_sharing,
|
|
1327
|
+
auto_downscale=auto_downscale,
|
|
1319
1328
|
)
|
|
1320
1329
|
|
|
1321
1330
|
@tool(annotations=ToolAnnotations(title="Get Document Structure", readOnlyHint=True))
|
{mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/emitter.py
RENAMED
|
@@ -910,12 +910,16 @@ async def _fill_children_recursive(
|
|
|
910
910
|
table_inserts.append((t, r, c, occurrence, child))
|
|
911
911
|
new_cursor = cursor + 1
|
|
912
912
|
else:
|
|
913
|
+
# Consume every non-Table child up to the next Table. An Image is
|
|
914
|
+
# skipped, as _build_fill_requests does for a cell with no nested
|
|
915
|
+
# table: table-cell images aren't supported (#509). Stopping only at
|
|
916
|
+
# a Table guarantees j > cursor, so the cell can't stay pending forever.
|
|
913
917
|
j = cursor
|
|
914
918
|
run_group: list[Run] = []
|
|
915
|
-
while j < len(children) and isinstance(children[j],
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
+
while j < len(children) and not isinstance(children[j], Table):
|
|
920
|
+
next_child = children[j]
|
|
921
|
+
if isinstance(next_child, Run):
|
|
922
|
+
run_group.append(next_child)
|
|
919
923
|
j += 1
|
|
920
924
|
reqs = _run_group_fill_requests(run_group, para_start)
|
|
921
925
|
if reqs:
|
|
@@ -917,9 +917,12 @@ class _AstParser(HTMLParser):
|
|
|
917
917
|
self._code_depth = max(0, self._code_depth - 1)
|
|
918
918
|
|
|
919
919
|
def handle_data(self, data):
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
920
|
+
# Table-cell text and open-block text are independent triggers that happen
|
|
921
|
+
# to be buffered the same way today; kept as separate branches so either
|
|
922
|
+
# can diverge without first untangling a merged condition (#638).
|
|
923
|
+
if self._table_depth >= 1 and self._table_stack and self._table_stack[-1].in_cell: # noqa: SIM114
|
|
924
|
+
self._run_buf.append(data)
|
|
925
|
+
elif self._block_tag:
|
|
923
926
|
self._run_buf.append(data)
|
|
924
927
|
elif self._table_depth == 0 and self._tag_depth == 0 and data.strip():
|
|
925
928
|
# Genuinely bare text — no wrapping block tag, no enclosing tag of
|
{mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/images.py
RENAMED
|
@@ -70,8 +70,12 @@ _FORMAT_MIME = {"PNG": "image/png", "JPEG": "image/jpeg", "GIF": "image/gif"}
|
|
|
70
70
|
|
|
71
71
|
# Every reason downscale_image_bytes can decline for an image already known to be
|
|
72
72
|
# oversized. "Too large to decode safely": Pillow's decompression-bomb guard refuses a
|
|
73
|
-
# full decode above ~179 megapixels, and downscaling needs one.
|
|
74
|
-
|
|
73
|
+
# full decode above ~179 megapixels, and downscaling needs one. "Still over the size
|
|
74
|
+
# limit": the bounded shrink loop ran out of attempts (#581).
|
|
75
|
+
_CANNOT_DOWNSCALE_REASONS = (
|
|
76
|
+
"unreadable or corrupt image, animated, too large to decode safely, "
|
|
77
|
+
"or still over the file-size limit after shrinking"
|
|
78
|
+
)
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
def _decode(data: bytes) -> Image.Image | None:
|
|
@@ -187,9 +191,11 @@ def downscale_image_bytes(data: bytes) -> tuple[bytes, str] | None:
|
|
|
187
191
|
"""Resize image `data` to fit both the megapixel and file-size limits, preserving
|
|
188
192
|
aspect ratio and format. Returns (resized_bytes, mime_type), or None if: the image
|
|
189
193
|
can't be decoded; it's already within both limits (nothing to do — caller should
|
|
190
|
-
keep using the original bytes/upload path);
|
|
194
|
+
keep using the original bytes/upload path); it's animated (Pillow's resize only
|
|
191
195
|
touches the current frame, which would silently drop every other frame of an
|
|
192
|
-
animated GIF — declining is safer than corrupting it)
|
|
196
|
+
animated GIF — declining is safer than corrupting it); or the bounded shrink loop
|
|
197
|
+
below still can't get it under the file-size limit (#581), so a caller never
|
|
198
|
+
embeds still-oversized bytes as if the resize had worked.
|
|
193
199
|
|
|
194
200
|
The byte-size limit can't be hit exactly on the first resize the way the
|
|
195
201
|
megapixel one can: encoded size depends on compression, not just pixel count, so a
|
|
@@ -236,6 +242,8 @@ def downscale_image_bytes(data: bytes) -> tuple[bytes, str] | None:
|
|
|
236
242
|
encoded = _encode(resized)
|
|
237
243
|
attempts += 1
|
|
238
244
|
|
|
245
|
+
if len(encoded) > MAX_INLINE_IMAGE_BYTES:
|
|
246
|
+
return None
|
|
239
247
|
return encoded, _FORMAT_MIME.get(fmt, "application/octet-stream")
|
|
240
248
|
|
|
241
249
|
|
|
@@ -249,17 +257,26 @@ def check_drive_image_metadata(metadata: dict[str, Any]) -> dict[str, Any] | Non
|
|
|
249
257
|
_resolve_image_source). Dimensions are checked first, matching
|
|
250
258
|
downscale_image_bytes's own resize-by-pixels-first behavior. Returns the first
|
|
251
259
|
violation found, or None if within both limits or the metadata doesn't report
|
|
252
|
-
enough to check (e.g. a non-image binary with no imageMediaMetadata
|
|
260
|
+
enough to check (e.g. a non-image binary with no imageMediaMetadata, or a
|
|
261
|
+
width/height/`size` that isn't a number). Never raises: both call sites call it
|
|
262
|
+
outside their try/except (#581)."""
|
|
253
263
|
img_meta = metadata.get("imageMediaMetadata") or {}
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
try:
|
|
265
|
+
width, height = int(img_meta["width"]), int(img_meta["height"])
|
|
266
|
+
except (KeyError, TypeError, ValueError):
|
|
267
|
+
# Missing or malformed dimensions mean unknown; fall through to the size check.
|
|
268
|
+
width = height = 0
|
|
269
|
+
if width > 0 and height > 0:
|
|
256
270
|
error = check_dimensions(width, height)
|
|
257
271
|
if error is not None:
|
|
258
272
|
return error
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
273
|
+
try:
|
|
274
|
+
size_bytes = int(metadata["size"])
|
|
275
|
+
except (KeyError, TypeError, ValueError):
|
|
276
|
+
# Drive sends `size` as a decimal string. Missing or malformed means unknown,
|
|
277
|
+
# the same as missing dimensions: let the embed call be the judge.
|
|
278
|
+
return None
|
|
279
|
+
return check_file_size(size_bytes)
|
|
263
280
|
|
|
264
281
|
|
|
265
282
|
def rewrite_too_large_error(message: str) -> str:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for docs content tools and HTML/Markdown pipeline."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import io
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
@@ -2400,6 +2401,25 @@ class TestCreateDocFromFile:
|
|
|
2400
2401
|
result = await _docs_tools["create_doc_from_file"](local_path=str(txt_file), ctx=ctx)
|
|
2401
2402
|
assert "error" in result
|
|
2402
2403
|
assert ".txt" in result["error"]
|
|
2404
|
+
# create_doc_from_file has no content_format parameter to suggest (#567).
|
|
2405
|
+
assert "content_format" not in result["error"]
|
|
2406
|
+
|
|
2407
|
+
@pytest.mark.parametrize("kind", ["directory", "latin1"])
|
|
2408
|
+
async def test_unreadable_source_returns_error(self, tmp_path, kind):
|
|
2409
|
+
# PR #961 QA round 1: a path that exists but isn't a readable UTF-8 file
|
|
2410
|
+
# returns {"error": ...} instead of raising, and creates nothing.
|
|
2411
|
+
if kind == "directory":
|
|
2412
|
+
src = tmp_path / "dir.md"
|
|
2413
|
+
src.mkdir()
|
|
2414
|
+
else:
|
|
2415
|
+
src = tmp_path / "latin1.html"
|
|
2416
|
+
src.write_bytes("<p>caf\xe9</p>".encode("latin-1"))
|
|
2417
|
+
drive_svc, docs_svc = self._make_services()
|
|
2418
|
+
ctx = self._ctx(drive_svc, docs_svc)
|
|
2419
|
+
result = await _docs_tools["create_doc_from_file"](local_path=str(src), ctx=ctx)
|
|
2420
|
+
assert "error" in result
|
|
2421
|
+
assert str(src) in result["error"]
|
|
2422
|
+
drive_svc.files.return_value.create.assert_not_called()
|
|
2403
2423
|
|
|
2404
2424
|
|
|
2405
2425
|
class TestUpdateDocFromFile:
|
|
@@ -2510,8 +2530,68 @@ class TestUpdateDocFromFile:
|
|
|
2510
2530
|
)
|
|
2511
2531
|
assert "error" in result
|
|
2512
2532
|
assert ".txt" in result["error"]
|
|
2533
|
+
assert "pass content_format explicitly" in result["error"]
|
|
2534
|
+
docs_svc.documents.return_value.get.assert_not_called()
|
|
2535
|
+
|
|
2536
|
+
@pytest.mark.parametrize("kind", ["directory", "latin1"])
|
|
2537
|
+
async def test_unreadable_source_returns_error(self, tmp_path, kind):
|
|
2538
|
+
# PR #961 QA round 1: same as create_doc_from_file; the doc is never touched.
|
|
2539
|
+
if kind == "directory":
|
|
2540
|
+
src = tmp_path / "dir.md"
|
|
2541
|
+
src.mkdir()
|
|
2542
|
+
else:
|
|
2543
|
+
src = tmp_path / "latin1.html"
|
|
2544
|
+
src.write_bytes("<p>caf\xe9</p>".encode("latin-1"))
|
|
2545
|
+
drive_svc, docs_svc = self._make_services()
|
|
2546
|
+
ctx = self._ctx(drive_svc, docs_svc)
|
|
2547
|
+
result = await _docs_tools["update_doc_from_file"](
|
|
2548
|
+
doc_id="doc1", local_path=str(src), ctx=ctx
|
|
2549
|
+
)
|
|
2550
|
+
assert "error" in result
|
|
2551
|
+
assert str(src) in result["error"]
|
|
2513
2552
|
docs_svc.documents.return_value.get.assert_not_called()
|
|
2514
2553
|
|
|
2554
|
+
def test_doc_content_flags_are_keyword_only(self):
|
|
2555
|
+
# #567 / PR #961 QA round 1: positional bools could be swapped at a call site
|
|
2556
|
+
# and still type-check, in both shared helpers.
|
|
2557
|
+
for fn, names in (
|
|
2558
|
+
(
|
|
2559
|
+
content_module._replace_doc_content,
|
|
2560
|
+
("autolink_urls", "revoke_sharing", "auto_downscale"),
|
|
2561
|
+
),
|
|
2562
|
+
(
|
|
2563
|
+
content_module._apply_doc_content,
|
|
2564
|
+
("autolink_urls", "target_folder_id", "revoke_sharing", "auto_downscale"),
|
|
2565
|
+
),
|
|
2566
|
+
):
|
|
2567
|
+
params = inspect.signature(fn).parameters
|
|
2568
|
+
for name in names:
|
|
2569
|
+
assert params[name].kind is inspect.Parameter.KEYWORD_ONLY, (fn.__name__, name)
|
|
2570
|
+
|
|
2571
|
+
@pytest.mark.parametrize(
|
|
2572
|
+
"flags",
|
|
2573
|
+
[
|
|
2574
|
+
{"autolink_urls": False, "revoke_sharing": True, "auto_downscale": True},
|
|
2575
|
+
{"autolink_urls": True, "revoke_sharing": False, "auto_downscale": False},
|
|
2576
|
+
],
|
|
2577
|
+
)
|
|
2578
|
+
async def test_flags_reach_apply_doc_content_unswapped(self, tmp_path, flags):
|
|
2579
|
+
# #567: _replace_doc_content's three bools are keyword-only, so each tool
|
|
2580
|
+
# flag must land in its own _apply_doc_content slot.
|
|
2581
|
+
md_file = tmp_path / "notes.md"
|
|
2582
|
+
md_file.write_text("text\n")
|
|
2583
|
+
drive_svc, docs_svc = self._make_services(end_index=2)
|
|
2584
|
+
ctx = self._ctx(drive_svc, docs_svc)
|
|
2585
|
+
real_apply = content_module._apply_doc_content
|
|
2586
|
+
with patch.object(content_module, "_apply_doc_content", return_value=None) as apply:
|
|
2587
|
+
await _docs_tools["update_doc_from_file"](
|
|
2588
|
+
doc_id="doc1", local_path=str(md_file), ctx=ctx, **flags
|
|
2589
|
+
)
|
|
2590
|
+
# Bind against the real signature so the assertion holds however the call
|
|
2591
|
+
# site passes them, and fails if the call wouldn't bind at all.
|
|
2592
|
+
bound = inspect.signature(real_apply).bind(*apply.call_args.args, **apply.call_args.kwargs)
|
|
2593
|
+
assert {k: bound.arguments[k] for k in flags} == flags
|
|
2594
|
+
|
|
2515
2595
|
async def test_clears_trailing_mark_style_before_deleting_in_its_own_batch(self, tmp_path):
|
|
2516
2596
|
# Same clear-then-insert mechanism as write_doc_content (#255) — verified here
|
|
2517
2597
|
# too since it's now shared via _replace_doc_content.
|