mcp-gee-sweet 0.9.2.dev313__tar.gz → 0.9.2.dev319__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.dev313 → mcp_gee_sweet-0.9.2.dev319}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/doc-to-markdown.md +3 -2
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/image-conversion.md +33 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/markdown-support.md +1 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/docs_content.md +111 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/roadmap.md +4 -3
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/ast.py +2 -2
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +63 -10
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/comments.py +14 -4
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/content.py +34 -7
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/emitter.py +66 -18
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/html_parser.py +51 -13
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_comments.py +41 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_content.py +173 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_core.py +153 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_markdown_export.py +139 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/notify-kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/notify-partner.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/qa-kickoff.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/roadmap-status.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/amy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/bob.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/joy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.dockerignore +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.gitignore +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/.python-version +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/CLAUDE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/Dockerfile +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/LICENSE +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/Makefile +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/assets/logo.png +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-comments-first-class.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-gmail-domain.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-local-fs-test-harness.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-pillow-image-dependency.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-prompt-qa-role.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/decision-tool-error-visibility.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/decisions/role-file-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/blockquote-representation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/borderless-table-columns.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/doc-revision-export.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/heading-anchor-resolution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/local-file-access-policy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/module-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design/sse-consent-off-event-loop.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/design.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/gmail-test-plan.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/results/2026-09-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/retro-v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/runs/v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/docs_layout.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/docs_style.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/docs_tables.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/drive_activity.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/drive_files.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/drive_sharing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/drive_transfer.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/gmail.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/style-guide.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/qa_cancel_insert_local_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/scripts/write_team_mcp_configs.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/server.json +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/http_transport.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/reauth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/atomic_write.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/pagination.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/fakes.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/integration/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/integration/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/integration/test_local_fs_harness.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_atomic_write.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_revision_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_reauth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_server_json.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/tests/test_team_mcp_configs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.9.2.
|
|
3
|
+
Version: 0.9.2.dev319
|
|
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
|
|
@@ -128,8 +128,9 @@ Three gaps were kept **as documented limitations, not implementation shortcuts**
|
|
|
128
128
|
- **Linked images** (#951): Docs puts an image's link on its `inlineObjectElement`'s own
|
|
129
129
|
`textStyle.link`, the same as a text run (confirmed live). `doc_to_ast` reads it into
|
|
130
130
|
`Image.link_url`, and every image renders through `_render_image` as `[](url)`, in
|
|
131
|
-
ordinary text, a code paragraph, and a table cell alike. The write side
|
|
132
|
-
|
|
131
|
+
ordinary text, a code paragraph, and a table cell alike. The write side sets it too (#977;
|
|
132
|
+
see [image-conversion.md](image-conversion.md#linked-images-977)), so a linked image
|
|
133
|
+
round-trips.
|
|
133
134
|
- **Same-document links** (PR #976 QA round 1): Docs reports a link to a heading or bookmark by
|
|
134
135
|
ID with no `url` (confirmed live for a heading: `{"headingId": "h.x"}`, on a text run and an
|
|
135
136
|
image alike). `doc_to_ast._link_url`, shared by both, rebuilds it as
|
|
@@ -103,6 +103,39 @@ need K retries. Resending the whole document each time would be K+1 full-size ro
|
|
|
103
103
|
Confirmed live that the split produces a document structurally identical to the old
|
|
104
104
|
whole-list retry (text, bullets, table, and the surviving image all in place).
|
|
105
105
|
|
|
106
|
+
## Linked images (#977)
|
|
107
|
+
|
|
108
|
+
An `<img>` inside `<a href>` (markdown `[](url)`) keeps its link: `html_parser.py`
|
|
109
|
+
copies the open `<a>`'s href onto `Image.link_url`. That includes raw HTML with no enclosing
|
|
110
|
+
block: a top-level `<img>` gets an implicit paragraph even when it's wrapped in inline tags (unlike
|
|
111
|
+
bare text or `<hr>` in that position), and reads its link from `_bare_link_url`, a stack separate
|
|
112
|
+
from `_link_url` so text handling and an unclosed top-level `<a>` are unaffected. Each open `<a>`
|
|
113
|
+
records which of the two stacks it pushed onto, or that it pushed nothing (`_anchor_pushes`), and
|
|
114
|
+
its `</a>` pops only that: an href-less `<a name>` nested in a linked anchor used to pop the outer
|
|
115
|
+
link (#994, PR #992 QA round 2). Docs stores an image's link on its
|
|
116
|
+
`inlineObjectElement`'s own `textStyle.link`, so `emitter.py` follows the image's
|
|
117
|
+
`insertInlineImage` with an `updateTextStyle` over its one-character range
|
|
118
|
+
`[position, position + 1)`. The link has to come right after its own insert, inside the
|
|
119
|
+
descending-position tail: at that point nothing before `position` has shifted yet. An inserted
|
|
120
|
+
image never inherits a neighboring run's link (confirmed live, both directions), so an unlinked
|
|
121
|
+
image gets no clearing request.
|
|
122
|
+
|
|
123
|
+
Inserts that share a position, such as adjacent images with nothing between them, each land in
|
|
124
|
+
front of whatever is already there, so the later one in the document has to be inserted first.
|
|
125
|
+
The descending pass sorts on `(position, node order, item order)`, all descending; sorting by
|
|
126
|
+
position alone kept source order within a tie and wrote adjacent images reversed (PR #992 QA
|
|
127
|
+
round 1). A table and the image right after it share a position the same way, and still come out
|
|
128
|
+
table first.
|
|
129
|
+
|
|
130
|
+
The retry treats the pair as one unit. `ast_to_requests(image_links=...)` records each
|
|
131
|
+
`id(insert request) -> link request` as it builds them, and `_apply_doc_content` uses that record
|
|
132
|
+
rather than matching request shapes. `_drop_failed_image` pops the link request with its failed
|
|
133
|
+
insert, since a stranded link would land on whatever character now sits at that position.
|
|
134
|
+
`_positional_insert_tail_start` counts a recorded link request as part of the tail, so the K≥2
|
|
135
|
+
split still applies. A `#slug` image link goes
|
|
136
|
+
through the heading-anchor pass like a text link: `_walk_headings_and_anchor_runs` reads the
|
|
137
|
+
`inlineObjectElement`'s link too, so it's resolved or stripped.
|
|
138
|
+
|
|
106
139
|
## Known, deliberate gap: table-cell images
|
|
107
140
|
|
|
108
141
|
`html_parser.py`'s img-handling only fires when a block context is open at the body level
|
|
@@ -173,6 +173,7 @@ another registered tool), passing `content_format` through to `_to_doc_requests`
|
|
|
173
173
|
| `~~strikethrough~~` | `<del>` | `Run(strikethrough=True)` | strikethrough |
|
|
174
174
|
| `[text](url)` | `<a href=...>` | `Run(link_url=...)` | hyperlink |
|
|
175
175
|
| bare `https://url` | `<a href=...>` (autolink extension) | `Run(link_url=...)` | hyperlink — disable via `autolink_urls=False` |
|
|
176
|
+
| `[](url)` | `<a href=...><img ...></a>` | `Image(link_url=...)` | linked inline image (#977) |
|
|
176
177
|
| `- item` | `<ul><li>` | `BulletItem(ordered=False)` | disc bullet list |
|
|
177
178
|
| `1. item` | `<ol><li>` | `BulletItem(ordered=True)` | numbered list |
|
|
178
179
|
| `- [x] item` | `<input type="checkbox" checked>` | `BulletItem(checked=True)` | bullet with a `☑`/`☐` glyph prefix |
|
|
@@ -3716,6 +3716,64 @@ Not covered live: a soft break (Shift+Enter) inside an inline code run in a para
|
|
|
3716
3716
|
|
|
3717
3717
|
**Result (2026-10-10) ✅ PASS — Kit, PR #981 round 2 (`b2bfafe`).** Ran on a scratch doc (created with `create_doc` from the step 1 HTML, trashed afterwards) instead of `{DOC_ID}`. Step 2 used one `find_in_doc(regex=True)` call for all three queries: matches at 4–11, 15–20 and 26–39. After step 3, `get_doc_structure` showed each of the first three paragraphs as two runs split at the `<`/`&` (`a <` | `b> text`, `AT&` | `amp;T`, `see <` | `https://x.io>`). `get_doc_as_markdown` returned `a \<b> text`, `AT\&T`, `see \<https://x.io>`, and `a < b and AT&T` unchanged. markdown-it-py `commonmark` rendered `a <b> text`, `AT&amp;T`, `see <https://x.io>`, and `a < b and AT&T`, with no `<b>` and no `<a>`. Round 1 got `a <b> text` and `AT&T` unescaped from the same kind of split, on pre-fix code (`307e47c`).
|
|
3718
3718
|
|
|
3719
|
+
|
|
3720
|
+
### TC-DOC216: A linked image written as Markdown or HTML keeps its link and round-trips (#977) ⚠️ requires-oauth
|
|
3721
|
+
**Background:** the write side built a linked image with no link, so `[](url)` and `<a href><img></a>` imported as a plain image and didn't round-trip with TC-DOC209's export. The image now gets its link through an `updateTextStyle` over its one-character range, right after its insert. That request has to stay paired with the image when an unreachable image is dropped and retried, and a `#slug` image link goes through the heading-anchor pass.
|
|
3722
|
+
|
|
3723
|
+
**Setup:** none. Each step uses a scratch doc.
|
|
3724
|
+
|
|
3725
|
+
**Prompt**
|
|
3726
|
+
**Playwright: required** (each linked image is clickable to its URL in the rendered doc; the unlinked one isn't)
|
|
3727
|
+
1. `create_doc(title="TC-DOC216 md", folder_id={FOLDER_ID}, content_format='markdown', content=...)` with:
|
|
3728
|
+
```markdown
|
|
3729
|
+
# Overview
|
|
3730
|
+
|
|
3731
|
+
Before [](https://example.com/one) and [linked text](https://example.com/t)
|
|
3732
|
+
|
|
3733
|
+
Jump: [](#overview) and [](#missing)
|
|
3734
|
+
|
|
3735
|
+
| a | b |
|
|
3736
|
+
|---|---|
|
|
3737
|
+
| 1 | 2 |
|
|
3738
|
+
|
|
3739
|
+
[](https://example.com/after-table)
|
|
3740
|
+
```
|
|
3741
|
+
2. `get_doc_as_markdown(doc_id=<step 1 docId>)`
|
|
3742
|
+
3. `create_doc(title="TC-DOC216 retry", folder_id={FOLDER_ID}, content_format='html', content=...)` with:
|
|
3743
|
+
```html
|
|
3744
|
+
<p><a href="https://example.com/b1"><img src="https://example.invalid/nope.png?1"></a></p><p>x <a href="https://example.com/good"><img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"></a> y</p><p><a href="https://example.com/b2"><img src="https://example.invalid/nope.png?2"></a></p>
|
|
3745
|
+
```
|
|
3746
|
+
4. `get_doc_as_markdown(doc_id=<step 3 docId>)`
|
|
3747
|
+
5. `create_doc(title="TC-DOC216 adjacent", folder_id={FOLDER_ID}, content_format='html', content=...)` with (raw HTML, the first image wrapped in a top-level `<a>` with no `<p>`):
|
|
3748
|
+
```html
|
|
3749
|
+
<a href="https://example.com/top"><img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" width="100"></a><p>x <a href="https://example.com/first"><img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" width="40"></a><a href="https://example.com/second"><img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" width="80"></a><img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" width="120"> y</p>
|
|
3750
|
+
```
|
|
3751
|
+
6. `get_doc_as_markdown(doc_id=<step 5 docId>)`
|
|
3752
|
+
7. `create_doc(title="TC-DOC216 nested anchor", folder_id={FOLDER_ID}, content_format='html', content=...)` with an href-less `<a name>` nested inside each linked anchor (#994):
|
|
3753
|
+
```html
|
|
3754
|
+
<a href="https://example.com/bare"><a name="n1"></a><img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"></a><p><a href="https://example.com/block"><a name="n2"></a>tail<img src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"></a> after</p>
|
|
3755
|
+
```
|
|
3756
|
+
8. `get_doc_as_markdown(doc_id=<step 7 docId>)`
|
|
3757
|
+
|
|
3758
|
+
**Checks**
|
|
3759
|
+
- Step 2: `[](https://example.com/one)`
|
|
3760
|
+
- Step 2: `[linked text](https://example.com/t)` is followed by an unlinked ``, not one inside the link
|
|
3761
|
+
- Step 2: image `A` links to `https://docs.google.com/document/d/<docId>/edit?tab=t.0#heading=<headingId>`, the `Overview` heading. Image `B` is unlinked, its `#missing` link stripped
|
|
3762
|
+
- Step 2: the image after the table is `[](https://example.com/after-table)`
|
|
3763
|
+
- Step 3: `images` reports an `error` for both `example.invalid` sources and none for the Google logo
|
|
3764
|
+
- Step 4: the only image is `[](https://example.com/good)`, between `x` and `y`. No text character carries `b1`'s or `b2`'s link
|
|
3765
|
+
- Step 6: the first paragraph is the top-level image alone, as `[](https://example.com/top)`
|
|
3766
|
+
- Step 6: the second paragraph is `x `, `[](https://example.com/first)`, `[](https://example.com/second)`, an unlinked ``, then ` y`, in that order. Playwright: in the step 5 doc's editor the three inline images get wider left to right (40, 80, 120px source widths), which confirms the order independently of the links
|
|
3767
|
+
- Step 8: the first paragraph is `[](https://example.com/bare)`. The second is `[tail](https://example.com/block)`, then `[](https://example.com/block)`, then an unlinked ` after`: the nested `<a name>` doesn't end the outer link
|
|
3768
|
+
|
|
3769
|
+
**Cleanup:** `delete_file` all four scratch docs
|
|
3770
|
+
|
|
3771
|
+
**Result (2026-10-11) ✅ PASS — Sky, PR #992 round 1 (`6aa4dac`).** Step 2: `Logo` came back as `[](https://example.com/one)`. `[linked text](https://example.com/t)` was followed by an unlinked ``. `A` linked to `.../edit?tab=t.0#heading=h.…` (the `Overview` heading), and `B` was unlinked. The image after the table kept `https://example.com/after-table`. Step 3: `images` reported an `error` for both `example.invalid` sources and none for the logo. Step 4: the only image was `[](https://example.com/good)` between `x` and `y`, and no text carried `b1`'s or `b2`'s link. Playwright, clicking each image in the step 1 doc's editor: `Logo` and the after-table image each showed a link chip for `https://example.com/…`, and `A`'s chip showed `Overview`. The unlinked image and `B` showed no link chip. The `/mobilebasic` HTML view isn't a substitute: it renders the text link but never wraps an image in `<a>`. A separate probe, `[](https://example.com/first)[](https://example.com/second)`, exported as `second` then `first`: adjacent images are written in reverse order. Each kept its own link.
|
|
3772
|
+
|
|
3773
|
+
**Result (2026-10-11) ✅ PASS — Sky, PR #992 round 2 (`3a506f4`).** Re-ran step 3 (the retry, now pairing each image with its link by identity) and the new step 5. Step 4: the only image was `[](https://example.com/good)` between `x` and `y`, and no text carried `b1`'s or `b2`'s link. Step 6: the first paragraph was the top-level image alone, as `[](https://example.com/top)`. The second paragraph was `x `, `…(https://example.com/first)`, `…(https://example.com/second)`, an unlinked image, then ` y`. Playwright: in the step 5 doc's editor the three inline images got wider left to right, and the top image's link chip showed `https://example.com/…`. Steps 1–2 weren't re-run: the fix commit doesn't change that path beyond the sort, which step 5 covers.
|
|
3774
|
+
|
|
3775
|
+
**Result (2026-10-11) ✅ PASS — Sky, PR #992 round 3 (`9271dfe`).** Ran the new step 7. Step 8: the first paragraph was `[](https://example.com/bare)`, and the second was `[tail](https://example.com/block)`, then `[](https://example.com/block)`, then an unlinked ` after`. The fix commit only touches `html_parser.py`'s anchor handling, so steps 1–6 weren't re-run.
|
|
3776
|
+
|
|
3719
3777
|
---
|
|
3720
3778
|
|
|
3721
3779
|
## Orphan fileId on inline-image sharing failure (#649)
|
|
@@ -4167,3 +4225,56 @@ Both tools read the local file through the shared `_read_doc_source_file` helper
|
|
|
4167
4225
|
**Cleanup:** `rm -rf /tmp/tc-doc213`
|
|
4168
4226
|
|
|
4169
4227
|
**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-doc213`. 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`).
|
|
4228
|
+
|
|
4229
|
+
## Comment quoted text is plain text, not HTML-escaped (#984)
|
|
4230
|
+
|
|
4231
|
+
Drive returns a comment's `quotedFileContent.value` HTML-escaped (`Café`, `&`, `<`, `"`, `'`), and a literal `&` in the anchor as `&amp;`. `add_doc_comment`, `list_doc_comments`, and `get_doc_as_markdown(include_comments=True)` unescape it once. The Markdown export then escapes the quote only as Markdown needs, with `"> "` on every line, and escapes comment authors, text, and replies the same way.
|
|
4232
|
+
|
|
4233
|
+
### TC-DOC215: Non-ASCII, `&`/`<`/`"`, literal-entity, and multi-line anchors round-trip as plain text ⚠️ destructive ⚠️ requires-oauth
|
|
4234
|
+
**Setup:** `create_doc(title="QA TC-DOC215", folder_id={FOLDER_ID}, content="<p>Café and Tom & Jerry, a < b, say \"hi\" it's &amp; literal</p><p>line one</p><p>line *two* <x></p>")`, note the returned `docId` as `{SCRATCH_DOC_ID}`
|
|
4235
|
+
|
|
4236
|
+
**Steps**
|
|
4237
|
+
1. `add_doc_comment(doc_id={SCRATCH_DOC_ID}, content="QA TC-DOC215 a <b> *x*", quoted_text="Café and Tom & Jerry, a < b, say \"hi\" it's")`
|
|
4238
|
+
2. `add_doc_comment(doc_id={SCRATCH_DOC_ID}, content="QA TC-DOC215 b", quoted_text="& literal")`
|
|
4239
|
+
3. `add_doc_comment(doc_id={SCRATCH_DOC_ID}, content="QA TC-DOC215 c", quoted_text="line one\nline *two* <x>")`
|
|
4240
|
+
4. `list_doc_comments(doc_id={SCRATCH_DOC_ID})`
|
|
4241
|
+
5. `get_doc_as_markdown(doc_id={SCRATCH_DOC_ID}, include_comments=True)`
|
|
4242
|
+
|
|
4243
|
+
**Checks**
|
|
4244
|
+
- Steps 1–3 each return `quoted_text` equal to the `quoted_text` passed in, character for character: `Café`, not `Café`; `&`, `<`, `"`, `'` unescaped; step 2 returns `& literal`, not `&amp; literal` or `& literal`
|
|
4245
|
+
- Step 4's three comments carry the same `quoted_text` values as steps 1–3
|
|
4246
|
+
- Step 5's `## Comments` section contains these quote blocks, each followed by a blank line before the comment text:
|
|
4247
|
+
- `> Café and Tom & Jerry, a < b, say "hi" it's`
|
|
4248
|
+
- `> \& literal`
|
|
4249
|
+
- `> line one\` (ending in a hard-break backslash, since PR #991 round 2) then `> line \*two\* \<x>` on the next line
|
|
4250
|
+
- Step 5 renders step 1's comment text as `QA TC-DOC215 a \<b> \*x\*` (plain text, escaped only as Markdown needs), and step 1 itself returns `content` unchanged
|
|
4251
|
+
- No `é`, `"`, `'`, or `<` appears anywhere in step 5's comments section
|
|
4252
|
+
|
|
4253
|
+
**Cleanup:** `delete_file(file_id={SCRATCH_DOC_ID})`
|
|
4254
|
+
|
|
4255
|
+
**Result (2026-10-11) ✅ PASS — Kit, PR #991 round 1 (`9953a83`).** Steps 1–3 echoed each `quoted_text` exactly (`Café and Tom & Jerry, a < b, say "hi" it's`, `& literal`, `line one\nline *two* <x>`); step 4 returned the same three values. Step 5's comments section had `> Café and Tom & Jerry, a < b, say "hi" it's`, `> \& literal`, and `> line one` / `> line \*two\* \<x>`, each followed by a blank line, and comment text `QA TC-DOC215 a \<b> \*x\*`; no `é`/`"`/`'`/`<` in the section. Extra probe: a comment created in the Docs UI (Playwright) on the doc's literal `& literal` text also came back from `list_doc_comments` as `& literal`, so Drive escapes UI-created anchors too and a single unescape is correct for them. The case passes, but the round was sent back for Markdown-escaping gaps in comment text that this case doesn't exercise (see the PR comment).
|
|
4256
|
+
|
|
4257
|
+
### TC-DOC217: Multi-line comment text and indented/rule body paragraphs stay plain text in the Markdown export ⚠️ destructive ⚠️ requires-oauth
|
|
4258
|
+
Comment text is plain text, so no line of it may render as Markdown structure, and each line keeps its own line (a hard break, not a soft wrap). Body paragraphs that start with spaces or a tab before a marker, or that are a `---` rule line, stay plain paragraphs too (#993). Replies are covered by unit tests only: the one tool that posts a reply, `resolve_doc_comment`, also resolves the comment, and `include_comments` exports only open ones.
|
|
4259
|
+
|
|
4260
|
+
**Setup:**
|
|
4261
|
+
- `create_doc(title="QA TC-DOC217", folder_id={FOLDER_ID}, content="<p>anchor</p>")`, note the returned `docId` as `{SCRATCH_DOC_ID}`
|
|
4262
|
+
- `insert_doc_text(doc_id={SCRATCH_DOC_ID}, insertions=[{"index": 1, "text": " - a\n # h\n---\n\ttab indent\n## Notes\n"}])`
|
|
4263
|
+
|
|
4264
|
+
**Steps**
|
|
4265
|
+
1. `add_doc_comment(doc_id={SCRATCH_DOC_ID}, quoted_text="anchor", content="Summary\n---\nPoints:\n - a\n - b\na | b\n-|-\nc | d\n## Notes\n\n indented *x*")`
|
|
4266
|
+
1a. `add_doc_comment(doc_id={SCRATCH_DOC_ID}, quoted_text="Summary\n=== ", content="QA TC-DOC217 setext")` (the trailing spaces after `===` are part of the anchor; Drive keeps them in `quoted_text`)
|
|
4267
|
+
2. `get_doc_as_markdown(doc_id={SCRATCH_DOC_ID}, include_comments=True, local_path="/tmp/tc-doc217.md")`
|
|
4268
|
+
3. `uv run python -c "from markdown_it import MarkdownIt; print(MarkdownIt('commonmark').enable('table').render(open('/tmp/tc-doc217.md').read()))"`
|
|
4269
|
+
|
|
4270
|
+
**Checks**
|
|
4271
|
+
- Step 3's HTML has no `<ul>`, `<ol>`, `<h1>`, `<table>`, or `<pre>`. It has exactly one `<h2>` (`Comments`) and one `<hr />`: the comments section's own heading and separator
|
|
4272
|
+
- The body renders `<p>- a</p>`, `<p># h</p>`, `<p>---</p>`, `<p>tab indent</p>`, and `<p>## Notes</p>`
|
|
4273
|
+
- Step 1's comment renders `Summary`, `---`, `Points:`, `- a`, `- b`, `a | b`, `-|-`, `c | d`, and `## Notes` as separate lines joined by `<br />` in one paragraph, with no literal `\` at any line end, then `<p>indented *x*</p>` as its own paragraph
|
|
4274
|
+
- Step 1a's quote renders as `<blockquote>` holding `<p>Summary<br />` and `===</p>`, with no `<h1>`
|
|
4275
|
+
|
|
4276
|
+
**Cleanup:** `delete_file(file_id={SCRATCH_DOC_ID})`, `rm /tmp/tc-doc217.md`
|
|
4277
|
+
|
|
4278
|
+
**Result (2026-10-11) ✅ PASS (case as written), ❌ sent back on probes — Kit, PR #991 round 2 (`de8143e`).** Ran with the export under this job's scratch dir instead of `/tmp`. Every check passed for the case's own content: body `<p>- a</p>`, `<p># h</p>`, `<p>---</p>`, `<p>tab indent</p>`; the comment's eight lines joined by `<br />` in one paragraph, then `<p>indented *x*</p>`; no `<ul>`/`<ol>`/`<table>`/`<pre>`. Extra probes on the same doc failed: (1) a body paragraph `## Notes` and a comment line `## Notes` each rendered as `<h2>`, and the comment line before it kept a literal `\`, because `_LEADING_SIMPLE_MARKER` matches only a single `#`; (2) `add_doc_comment(quoted_text="Summary\n=== ")` exported `> Summary\` / `> === `, which renders as `<h1>Summary\</h1>` inside the blockquote, because `_LEADING_RULE_LINE` doesn't allow trailing whitespace after `=`. Drive trims trailing whitespace from the end of comment `content` (not from `quoted_text`), and a mid-content `=== ` line is safe because the hard break before it blocks the setext.
|
|
4279
|
+
|
|
4280
|
+
**Result (2026-10-11) ✅ PASS — Kit, PR #991 round 3 (`95ba882`).** Ran with the export under this job's scratch dir instead of `/tmp`. Step 3's HTML had no `<ul>`/`<ol>`/`<h1>`/`<table>`/`<pre>`, exactly one `<h2>` (`Comments`) and one `<hr />`. Body: `<p>- a</p>`, `<p># h</p>`, `<p>---</p>`, `<p>tab indent</p>`, `<p>## Notes</p>`. Step 1's comment rendered its nine lines through `## Notes` joined by `<br />` with no literal `\`, then `<p>indented *x*</p>`. Step 1a's `quoted_text` came back as `Summary\n=== ` (trailing spaces kept) and rendered as `<blockquote><p>Summary<br />===</p></blockquote>`, no `<h1>`.
|
|
@@ -224,8 +224,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
224
224
|
- [x] `add_chart`: a cell-to-row (`B2:5`) or row-only (`1:5`) range silently charts one column — `_per_column_ranges` defaults `endColumnIndex` to start+1. Found reviewing #931 — lane A, after #917 (PR #983) ([#954](https://github.com/khuisman/mcp-gee-sweet/issues/954))
|
|
225
225
|
- [x] `_parse_a1_notation` hardening: one compiled grammar, index bounds, validate before the sheet-ID fetch. #931 review follow-ups, no behavior change for valid input — lane A, after #954 (same A1 area); its QA also runs #953's live checks for #931 (folded in) (PR #986) ([#955](https://github.com/khuisman/mcp-gee-sweet/issues/955))
|
|
226
226
|
- [ ] Write side drops image links: `[](url)` (or `<a href><img></a>`) imports through `create_doc`/`create_doc_from_file`/`write_doc_content` without its URL, so it doesn't round-trip with #951's export. Found in PR #976 — lane A, after #955 ([#977](https://github.com/khuisman/mcp-gee-sweet/issues/977))
|
|
227
|
-
- [
|
|
228
|
-
- [ ] Comment quoted text comes back HTML-escaped (`Café`) from `add_doc_comment`, `list_doc_comments`, and `get_doc_as_markdown(include_comments=True)`: `comments.py` passes Drive's escaped `quotedFileContent.value` through unchanged. Found reviewing #958 — lane B, after #959 (defect, next pick) ([#984](https://github.com/khuisman/mcp-gee-sweet/issues/984))
|
|
227
|
+
- [x] Comment quoted text comes back HTML-escaped (`Café`) from `add_doc_comment`, `list_doc_comments`, and `get_doc_as_markdown(include_comments=True)`: `comments.py` passes Drive's escaped `quotedFileContent.value` through unchanged. Found reviewing #958 — lane B, after #959 (defect, next pick) (PR #991) ([#984](https://github.com/khuisman/mcp-gee-sweet/issues/984))
|
|
229
228
|
- [ ] `write_capped_result_to_disk` writes `dir/` as a file when the directory doesn't exist: a trailing separator should mean "directory" (create it, append `default_filename`). Shared by every `local_path` writer. Found in PR #988 — lane B, after #984 (defect) ([#990](https://github.com/khuisman/mcp-gee-sweet/issues/990))
|
|
230
229
|
|
|
231
230
|
**Sheets hardening** _(triaged out of `backlog` 2026-09-11 — see the "Backlog triage" note below)_
|
|
@@ -252,7 +251,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
252
251
|
- [ ] **Bundle:** Docs read side (`doc_to_ast`/`ast_to_markdown`), one PR closing [#596](https://github.com/khuisman/mcp-gee-sweet/issues/596), [#600](https://github.com/khuisman/mcp-gee-sweet/issues/600), [#732](https://github.com/khuisman/mcp-gee-sweet/issues/732) — lane B ([#893](https://github.com/khuisman/mcp-gee-sweet/issues/893))
|
|
253
252
|
- [ ] `write_doc_content`/`update_doc_from_file` leave the doc empty when the replacement fails after the clear — decided 2026-10-04: record the pre-clear revision ID and name it in the error so the caller can recover via `export_revision` ([#878](https://github.com/khuisman/mcp-gee-sweet/issues/878))
|
|
254
253
|
- [ ] `update_doc_from_file`: opt-in incremental (`mode="patch"`) update that diffs block by block and rewrites only changed blocks, so comment anchors and revision history survive — lane A, after #894 (same write-path files) ([#957](https://github.com/khuisman/mcp-gee-sweet/issues/957))
|
|
255
|
-
- [
|
|
254
|
+
- [x] `export_revision`: support Google Docs revisions (returned as markdown), and fix `list_revisions`' docstring claim that native-file revisions are pruned after 30 days. Also what #878's recovery path points callers at — lane B, after #958 (PR #988) ([#959](https://github.com/khuisman/mcp-gee-sweet/issues/959))
|
|
256
255
|
- [ ] `export_revision`'s temporary Doc exposes an old revision's content to the default folder's audience (a whole Shared Drive) — decided 2026-10-11: under OAuth create it in My Drive root; a service account keeps the default folder, and that remaining exposure goes in `docs/known-limitations.md`. Found in PR #988 — lane B, after #990 ([#989](https://github.com/khuisman/mcp-gee-sweet/issues/989))
|
|
257
256
|
- [x] `insert_local_images`: temporary `anyone:reader` image shares aren't revoked when the call is cancelled — apply PR #877's shielded-cleanup pattern — lane B ([#883](https://github.com/khuisman/mcp-gee-sweet/issues/883)) (PR #903)
|
|
258
257
|
- [ ] **Bundle:** Docs image-share cancellation follow-ups (from PR #903), one PR closing [#904](https://github.com/khuisman/mcp-gee-sweet/issues/904), [#907](https://github.com/khuisman/mcp-gee-sweet/issues/907), [#908](https://github.com/khuisman/mcp-gee-sweet/issues/908), [#666](https://github.com/khuisman/mcp-gee-sweet/issues/666) — lane A ([#909](https://github.com/khuisman/mcp-gee-sweet/issues/909))
|
|
@@ -380,6 +379,8 @@ Follows the design in [`docs/decisions/decision-comments-first-class.md`](decisi
|
|
|
380
379
|
|
|
381
380
|
- [ ] `sync_folder(export_format=...)`: converted Sheets/Slides probably have #814's `modifiedTime` lag and restamp race too. Suspected, not yet observed ([#818](https://github.com/khuisman/mcp-gee-sweet/issues/818))
|
|
382
381
|
- [ ] Add a tool to set (and read back) Google Docs table column widths — `tableColumnProperties` isn't reachable through any tool today ([#809](https://github.com/khuisman/mcp-gee-sweet/issues/809))
|
|
382
|
+
- [ ] `_parse_a1_notation` rejects `A:B5`, which the Sheets values API accepts as `A5:B` — decided 2026-10-11: keep rejecting (a destructive GridRange tool acting on "row 5 onward" is worse than an error); document in the docstring that this is deliberately stricter than Sheets. Found in PR #986 — lane A; moved from v0.9.2 2026-10-11 (docstring only, current behavior is the safe one) ([#987](https://github.com/khuisman/mcp-gee-sweet/issues/987))
|
|
383
|
+
- [ ] `html_parser`: the `</a>` of an href-less anchor nested in a linked one pops the outer link, so text or an image after it is written unlinked. Found in PR #992's QA, which sent back the same bug in its new `_bare_link_url` stack; may close with PR #992 — lane A; moved from v0.9.2 2026-10-11 (pre-existing, invalid nested HTML only) ([#994](https://github.com/khuisman/mcp-gee-sweet/issues/994))
|
|
383
384
|
|
|
384
385
|
**Code structure & typing (Joy)** _(slotted into v0.9.2 (now v0.9.3) 2026-09-09 so Joy's architecture initiative carries release deadlines instead of staying open-ended)_
|
|
385
386
|
|
{mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/ast.py
RENAMED
|
@@ -49,8 +49,8 @@ class Image:
|
|
|
49
49
|
alt: str | None = None
|
|
50
50
|
width: float | None = None # pt
|
|
51
51
|
height: float | None = None # pt
|
|
52
|
-
#
|
|
53
|
-
#
|
|
52
|
+
# The image element's own textStyle.link: read by doc_to_ast.py, and written
|
|
53
|
+
# by emitter.py as an updateTextStyle over the image's one-character range (#977).
|
|
54
54
|
link_url: str | None = None
|
|
55
55
|
|
|
56
56
|
|
|
@@ -57,8 +57,12 @@ _BACKTICK_RUN = re.compile(r"`+")
|
|
|
57
57
|
# Paragraph rendering only: Heading/NamedBlock/BulletItem already have their
|
|
58
58
|
# own legitimate prefix on the same line, so content following it is never
|
|
59
59
|
# re-parsed as a new block starting mid-line.
|
|
60
|
-
_LEADING_SIMPLE_MARKER = re.compile(r"^(
|
|
60
|
+
_LEADING_SIMPLE_MARKER = re.compile(r"^(#{1,6}|[>*+-])(?=[ \t]|$)")
|
|
61
61
|
_LEADING_ORDERED_MARKER = re.compile(r"^(\d{1,9})([.)])(?=[ \t]|$)")
|
|
62
|
+
# A line of only "-" (spaces allowed) is a thematic break, or a setext
|
|
63
|
+
# underline under a preceding line, and so is a line of only "=". "*" and "_"
|
|
64
|
+
# rules can't occur: _escape has already escaped both (#993).
|
|
65
|
+
_LEADING_RULE_LINE = re.compile(r"^(?:=+[ \t]*|-[- \t]*)$")
|
|
62
66
|
|
|
63
67
|
_NESTED_TABLE_PLACEHOLDER = (
|
|
64
68
|
"*(nested table omitted — Markdown tables can't contain a table; "
|
|
@@ -162,14 +166,24 @@ def _render_block(node: Heading | Paragraph | BulletItem | NamedBlock) -> str:
|
|
|
162
166
|
|
|
163
167
|
depth = getattr(node, "blockquote_depth", 0)
|
|
164
168
|
if depth:
|
|
165
|
-
|
|
166
|
-
body = "\n".join(
|
|
167
|
-
f"{prefix}{line}" if line else prefix.rstrip() for line in body.split("\n")
|
|
168
|
-
)
|
|
169
|
+
body = _blockquote(body, depth)
|
|
169
170
|
return body
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
def _blockquote(body: str, depth: int) -> str:
|
|
174
|
+
prefix = "> " * depth
|
|
175
|
+
return "\n".join(f"{prefix}{line}" if line else prefix.rstrip() for line in body.split("\n"))
|
|
176
|
+
|
|
177
|
+
|
|
172
178
|
def _escape_leading_block_marker(text: str) -> str:
|
|
179
|
+
"""Make one line that starts a block read as paragraph text. Leading
|
|
180
|
+
spaces and tabs are dropped first: CommonMark strips a paragraph's
|
|
181
|
+
leading whitespace anyway, so the rendered text is unchanged, while
|
|
182
|
+
keeping them lets " - a" parse as a list and four spaces or a tab as an
|
|
183
|
+
indented code block (#993)."""
|
|
184
|
+
text = text.lstrip(" \t")
|
|
185
|
+
if _LEADING_RULE_LINE.match(text):
|
|
186
|
+
return "\\" + text
|
|
173
187
|
m = _LEADING_SIMPLE_MARKER.match(text)
|
|
174
188
|
if m:
|
|
175
189
|
return "\\" + text
|
|
@@ -463,12 +477,51 @@ def _table_safe(text: str) -> str:
|
|
|
463
477
|
|
|
464
478
|
|
|
465
479
|
def _render_comment(comment: dict) -> str:
|
|
466
|
-
|
|
480
|
+
"""Comment authors, anchors, and text are all plain text (Drive's
|
|
481
|
+
`content`, not `htmlContent`), so none of it may render as Markdown
|
|
482
|
+
structure (#984). See _comment_text for how each line is kept literal.
|
|
483
|
+
|
|
484
|
+
The quote is a blockquote ended by a blank line, since the comment text
|
|
485
|
+
after it would otherwise be a lazy continuation of the quote. Without a
|
|
486
|
+
quote, the author line ends in a hard break so the text starts on its own
|
|
487
|
+
line. A reply's continuation lines are indented to the list item's
|
|
488
|
+
content column, so they stay in the item instead of ending the list."""
|
|
489
|
+
author = _comment_inline((comment.get("author") or {}).get("display_name") or "Unknown")
|
|
490
|
+
content = _comment_text(comment.get("content") or "")
|
|
467
491
|
lines = [f"**{author}**"]
|
|
468
492
|
if comment.get("quoted_text"):
|
|
469
|
-
lines.append(
|
|
470
|
-
|
|
493
|
+
lines.append(_blockquote(_comment_text(comment["quoted_text"]), 1) + "\n")
|
|
494
|
+
elif content.split("\n", 1)[0]:
|
|
495
|
+
lines[0] += "\\"
|
|
496
|
+
lines.append(content)
|
|
471
497
|
for reply in comment.get("replies", []):
|
|
472
|
-
r_author = (reply.get("author") or {}).get("display_name") or "Unknown"
|
|
473
|
-
|
|
498
|
+
r_author = _comment_inline((reply.get("author") or {}).get("display_name") or "Unknown")
|
|
499
|
+
first, *rest = _comment_text(reply.get("content") or "", starts_block=False).split("\n")
|
|
500
|
+
lines.append(
|
|
501
|
+
"\n".join(
|
|
502
|
+
[f" - **{r_author}**: {first}"] + [f" {line}" if line else "" for line in rest]
|
|
503
|
+
)
|
|
504
|
+
)
|
|
474
505
|
return "\n".join(lines) + "\n"
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _comment_inline(text: str) -> str:
|
|
509
|
+
# "|" too: consecutive comment lines like "a | b" and "-|-" form a table.
|
|
510
|
+
return _escape(text).replace("|", "\\|")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _comment_text(text: str, starts_block: bool = True) -> str:
|
|
514
|
+
"""Multi-line plain text as Markdown that renders as the same lines. Each
|
|
515
|
+
line is escaped inline, and block markers are escaped on every line that
|
|
516
|
+
can start a block: all of them, except a first line that follows other
|
|
517
|
+
text on its own line (a reply's, after "**author**: "). Every line
|
|
518
|
+
followed by a non-empty one ends in a hard break ("\\"); a soft break
|
|
519
|
+
would join them into one line. An empty line stays a paragraph break."""
|
|
520
|
+
lines = [_comment_inline(line) for line in text.split("\n")]
|
|
521
|
+
for i in range(len(lines)):
|
|
522
|
+
if i or starts_block:
|
|
523
|
+
lines[i] = _escape_leading_block_marker(lines[i])
|
|
524
|
+
for i in range(len(lines) - 1):
|
|
525
|
+
if lines[i] and lines[i + 1]:
|
|
526
|
+
lines[i] += "\\"
|
|
527
|
+
return "\n".join(lines)
|
{mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/comments.py
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import html
|
|
1
2
|
import logging
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
@@ -38,8 +39,18 @@ def _map_reply(reply: dict[str, Any]) -> dict[str, Any]:
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
def _quoted_text(resource: dict[str, Any]) -> str | None:
|
|
43
|
+
"""The plain text a comment is anchored to. Drive returns
|
|
44
|
+
quotedFileContent.value HTML-escaped ("Café", "&") even though
|
|
45
|
+
it was written as plain text, and escapes a literal "&" in the
|
|
46
|
+
anchor as "&amp;", so unescaping once restores it exactly (#984)."""
|
|
47
|
+
quoted = resource.get("quotedFileContent")
|
|
48
|
+
if not quoted or quoted.get("value") is None:
|
|
49
|
+
return None
|
|
50
|
+
return html.unescape(quoted["value"])
|
|
51
|
+
|
|
52
|
+
|
|
41
53
|
def _map_comment(comment: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
-
quoted = comment.get("quotedFileContent")
|
|
43
54
|
return {
|
|
44
55
|
"id": comment.get("id"),
|
|
45
56
|
"content": comment.get("content"),
|
|
@@ -48,7 +59,7 @@ def _map_comment(comment: dict[str, Any]) -> dict[str, Any]:
|
|
|
48
59
|
"modified_time": comment.get("modifiedTime"),
|
|
49
60
|
"resolved": comment.get("resolved", False),
|
|
50
61
|
"deleted": comment.get("deleted", False),
|
|
51
|
-
"quoted_text":
|
|
62
|
+
"quoted_text": _quoted_text(comment),
|
|
52
63
|
"replies": [_map_reply(r) for r in comment.get("replies", [])],
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -155,14 +166,13 @@ def register(tool):
|
|
|
155
166
|
drive_service,
|
|
156
167
|
)
|
|
157
168
|
|
|
158
|
-
quoted = result.get("quotedFileContent")
|
|
159
169
|
logger.debug("Added comment %s to doc %s", result.get("id"), doc_id)
|
|
160
170
|
return {
|
|
161
171
|
"id": result.get("id"),
|
|
162
172
|
"content": result.get("content"),
|
|
163
173
|
"author": _map_author(result.get("author")),
|
|
164
174
|
"created_time": result.get("createdTime"),
|
|
165
|
-
"quoted_text":
|
|
175
|
+
"quoted_text": _quoted_text(result),
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
@tool(annotations=ToolAnnotations(title="Resolve Document Comment", destructiveHint=True))
|
{mcp_gee_sweet-0.9.2.dev313 → mcp_gee_sweet-0.9.2.dev319}/src/mcp_gee_sweet/tools/docs/content.py
RENAMED
|
@@ -162,12 +162,20 @@ def _failed_insert_image_message(error: HttpError) -> str:
|
|
|
162
162
|
_POSITIONAL_INSERT_KEYS = ("insertTable", "insertInlineImage")
|
|
163
163
|
|
|
164
164
|
|
|
165
|
-
def _positional_insert_tail_start(
|
|
165
|
+
def _positional_insert_tail_start(
|
|
166
|
+
requests: list[dict], image_links: dict[int, dict] | None = None
|
|
167
|
+
) -> int:
|
|
166
168
|
"""Index where ast_to_requests's trailing stage-3 pass (the descending-position
|
|
167
169
|
insertTable + insertInlineImage requests — see its docstring) begins: the start
|
|
168
|
-
of the maximal suffix made up only of those two request kinds
|
|
170
|
+
of the maximal suffix made up only of those two request kinds, plus each linked
|
|
171
|
+
image's own link request (#977). `image_links` is the id(insert) -> link
|
|
172
|
+
request map ast_to_requests filled; a link request counts by identity."""
|
|
173
|
+
link_ids = {id(link) for link in (image_links or {}).values()}
|
|
169
174
|
start = len(requests)
|
|
170
|
-
while start > 0 and
|
|
175
|
+
while start > 0 and (
|
|
176
|
+
id(requests[start - 1]) in link_ids
|
|
177
|
+
or any(key in requests[start - 1] for key in _POSITIONAL_INSERT_KEYS)
|
|
178
|
+
):
|
|
171
179
|
start -= 1
|
|
172
180
|
return start
|
|
173
181
|
|
|
@@ -553,8 +561,9 @@ async def _apply_doc_content(
|
|
|
553
561
|
# (#789): revoking after a failed edit is harmless, since a doc that did embed
|
|
554
562
|
# an image holds its own copy and one that didn't never needed the share.
|
|
555
563
|
try:
|
|
564
|
+
image_links: dict[int, dict] = {}
|
|
556
565
|
content_requests, tables = ast_to_requests(
|
|
557
|
-
nodes, start_index=1, image_uris=placeholder_uris
|
|
566
|
+
nodes, start_index=1, image_uris=placeholder_uris, image_links=image_links
|
|
558
567
|
)
|
|
559
568
|
|
|
560
569
|
# Swap each request's placeholder URI for the real one, recording which
|
|
@@ -626,13 +635,21 @@ async def _apply_doc_content(
|
|
|
626
635
|
|
|
627
636
|
def _drop_failed_image(requests: list[dict], e: HttpError) -> bool:
|
|
628
637
|
"""Pop the insertInlineImage request `e` names from `requests` (the
|
|
629
|
-
batch that raised `e`),
|
|
638
|
+
batch that raised `e`), plus its link request if the image was
|
|
639
|
+
linked (#977), recording the failure on its image's outcome
|
|
630
640
|
entry. False if `e` isn't an image-fetch failure, so the caller
|
|
631
641
|
re-raises."""
|
|
632
642
|
bad_index = _failed_insert_image_request_index(e)
|
|
633
643
|
if bad_index is None or bad_index >= len(requests):
|
|
634
644
|
return False
|
|
635
645
|
bad_request = requests.pop(bad_index)
|
|
646
|
+
# Left in place, the link would land on whatever character now
|
|
647
|
+
# sits at the image's position. Matched by identity: the list
|
|
648
|
+
# holds the same dict objects ast_to_requests recorded.
|
|
649
|
+
link = image_links.get(id(bad_request))
|
|
650
|
+
if link is not None:
|
|
651
|
+
link_index = next(i for i, r in enumerate(requests) if r is link)
|
|
652
|
+
requests.pop(link_index)
|
|
636
653
|
img_id = request_id_to_img_id.get(id(bad_request))
|
|
637
654
|
entry = entry_by_id.get(img_id) if img_id is not None else None
|
|
638
655
|
if entry is not None:
|
|
@@ -660,7 +677,7 @@ async def _apply_doc_content(
|
|
|
660
677
|
image_failures += 1
|
|
661
678
|
if image_failures != 2:
|
|
662
679
|
continue
|
|
663
|
-
tail_start = _positional_insert_tail_start(remaining)
|
|
680
|
+
tail_start = _positional_insert_tail_start(remaining, image_links)
|
|
664
681
|
# Fail-safe: split only if every image request really is in the
|
|
665
682
|
# tail. If ast_to_requests ever emits something after its
|
|
666
683
|
# positional-insert pass, the boundary scan comes up short —
|
|
@@ -728,7 +745,7 @@ def _walk_headings_and_anchor_runs(
|
|
|
728
745
|
) -> tuple[list[tuple[str, str]], list[tuple[str, int, int]]]:
|
|
729
746
|
"""Recursively walk document body content, including table cells,
|
|
730
747
|
collecting (heading_text, heading_id) pairs and (anchor_url, start, end)
|
|
731
|
-
runs for any run whose link_url starts with '#'.
|
|
748
|
+
runs for any run or inline image whose link_url starts with '#'.
|
|
732
749
|
|
|
733
750
|
Mirrors _collect_doc_paragraphs's two correctness properties: it recurses
|
|
734
751
|
into table cells (a heading or an anchor link living inside a table cell
|
|
@@ -767,6 +784,16 @@ def _walk_headings_and_anchor_runs(
|
|
|
767
784
|
end = pe.get("endIndex")
|
|
768
785
|
if end is not None:
|
|
769
786
|
offset = end
|
|
787
|
+
# A linked image carries its link on the inlineObjectElement's
|
|
788
|
+
# own textStyle (#977).
|
|
789
|
+
link_url = (
|
|
790
|
+
pe.get("inlineObjectElement", {})
|
|
791
|
+
.get("textStyle", {})
|
|
792
|
+
.get("link", {})
|
|
793
|
+
.get("url", "")
|
|
794
|
+
)
|
|
795
|
+
if link_url.startswith("#") and end is not None:
|
|
796
|
+
anchor_runs.append((link_url, run_start, end))
|
|
770
797
|
|
|
771
798
|
if is_heading and heading_id:
|
|
772
799
|
headings.append(("".join(text_parts).strip(), heading_id))
|