mcp-gee-sweet 0.8.2.dev71__tar.gz → 0.8.2.dev73__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.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CLAUDE.md +1 -1
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/configuration.md +2 -2
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/docs.md +85 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/roadmap.md +2 -2
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/tools.md +2 -1
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/scripts/gen_tool_docs.py +1 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/content.py +161 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_content.py +341 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/amy.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/kai.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.dockerignore +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.gitignore +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.python-version +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/Dockerfile +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/LICENSE +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/Makefile +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/README.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/index.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/drive.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/http_transport.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_comments.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/uv.lock +0 -0
|
@@ -39,7 +39,7 @@ Logic is split across `src/mcp_gee_sweet/`: `server.py` (MCP setup, tool decorat
|
|
|
39
39
|
- `tools/drive/transfer.py` — upload/download/sync/export/revisions
|
|
40
40
|
- `tools/drive/activity.py` — `list_file_activity` (Drive Activity API v2; requires `drive.activity.readonly` scope)
|
|
41
41
|
- `tools/docs/` — Google Docs package; `__init__.py` is a thin dispatcher that delegates to four submodules:
|
|
42
|
-
- `docs/content.py` — helpers (`_html_to_text`, `_md_to_html`, `_to_doc_requests`, `_html_to_doc_requests`) and tools (`create_doc`, `get_doc_content`, `write_doc_content`, `create_doc_from_file`, `insert_doc_text`, `delete_doc_range`, `get_doc_structure`, `insert_inline_image`, `insert_page_break`, `create_named_range`, `create_bookmark`). The Docs API has no bookmark-creation endpoint — `create_bookmark` is a thin wrapper over `createNamedRange` spanning a single character; neither tool's output is a link target (only UI-created bookmarks/headings are)
|
|
42
|
+
- `docs/content.py` — helpers (`_html_to_text`, `_md_to_html`, `_to_doc_requests`, `_html_to_doc_requests`, `_collect_doc_paragraphs`) and tools (`create_doc`, `get_doc_content`, `write_doc_content`, `create_doc_from_file`, `insert_doc_text`, `delete_doc_range`, `get_doc_structure`, `find_in_doc`, `insert_inline_image`, `insert_page_break`, `create_named_range`, `create_bookmark`). The Docs API has no bookmark-creation endpoint — `create_bookmark` is a thin wrapper over `createNamedRange` spanning a single character; neither tool's output is a link target (only UI-created bookmarks/headings are). `find_in_doc` searches paragraph text (including table cells) and returns `start_index`/`end_index` offsets in UTF-16 code units usable directly with `style_doc_range` — `_collect_doc_paragraphs` is a generator (so a caller can stop early once `max_results` is hit) that walks the Docs API body, trusting each `ParagraphElement`'s own `startIndex` when present and otherwise carrying the running offset forward rather than dropping that element, since the API doesn't always populate it (seen on a document's first element)
|
|
43
43
|
- `docs/tables.py` — tools (`insert_doc_table`, `insert_table_row`, `delete_table_row`, `insert_table_column`, `delete_table_column`, `merge_table_cells`)
|
|
44
44
|
- `docs/style.py` — helpers (`_NAMED_STYLE_TYPES`, `_read_body_styles`, `_read_named_styles`, `_build_named_style_requests`) and tools (`style_doc_range`, `style_doc_table_cells`, `get_doc_theme`, `get_doc_named_styles`, `apply_theme`)
|
|
45
45
|
- `docs/layout.py` — tools (`create_header`, `create_footer`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.8.2.
|
|
3
|
+
Version: 0.8.2.dev73
|
|
4
4
|
Summary: MCP server for Google Workspace — Sheets, Drive, Docs, and Calendar. 60+ tools for AI clients.
|
|
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
|
|
@@ -70,7 +70,7 @@ Access logs are emitted at `INFO` level — they appear when `DEBUG_LEVEL` is `D
|
|
|
70
70
|
|
|
71
71
|
<!-- Auto-generated by `scripts/gen_tool_docs.py` — do not edit by hand, your changes will be overwritten on the next commit. -->
|
|
72
72
|
|
|
73
|
-
By default all
|
|
73
|
+
By default all 109 tools are registered. Use `ENABLED_TOOLS` (or `--include-tools` on the CLI) to restrict the server to exactly the tools you need. This reduces the AI's context window cost — each registered tool is a name the model must reason about on every call.
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
# Environment variable
|
|
@@ -96,7 +96,7 @@ get_sheet_data,get_sheet_formulas,get_multiple_sheet_data,get_multiple_spreadshe
|
|
|
96
96
|
|
|
97
97
|
**Docs only:**
|
|
98
98
|
```
|
|
99
|
-
create_doc,get_doc_content,get_doc_structure,write_doc_content,insert_doc_text,insert_doc_table,delete_doc_range,style_doc_range,style_doc_table_cells
|
|
99
|
+
create_doc,get_doc_content,get_doc_structure,find_in_doc,write_doc_content,insert_doc_text,insert_doc_table,delete_doc_range,style_doc_range,style_doc_table_cells
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
**Calendar only:**
|
|
@@ -1932,3 +1932,88 @@ Returned `{"error": "<HttpError 400 ... Index 99998 must be less than the end in
|
|
|
1932
1932
|
|
|
1933
1933
|
**Result (2026-07-17) ✅ PASS**
|
|
1934
1934
|
Returned `{"error": "<HttpError 400 ... Index 99999 must be less than the end index of the referenced segment, 65.>"}` — clean error dict, no exception, references index out of bounds.
|
|
1935
|
+
|
|
1936
|
+
---
|
|
1937
|
+
|
|
1938
|
+
### TC-DOC111: `find_in_doc` literal case-insensitive search returns correct offsets
|
|
1939
|
+
**Setup:** create a doc via `write_doc_content` with content `<p>Hello World</p><p>another hello here</p>`
|
|
1940
|
+
|
|
1941
|
+
**Prompt**
|
|
1942
|
+
> "Find all instances of 'hello' in doc {DOC_ID}"
|
|
1943
|
+
|
|
1944
|
+
**Checks**
|
|
1945
|
+
- Returns a list of 2 matches
|
|
1946
|
+
- First match: `matched_text` is `"Hello"`, `context` is `"Hello World"`
|
|
1947
|
+
- Second match: `matched_text` is `"hello"`, `context` is `"another hello here"`
|
|
1948
|
+
- Each match's `start_index`/`end_index` span exactly the matched text — confirm by calling `get_doc_structure` and slicing the paragraph text at those offsets
|
|
1949
|
+
|
|
1950
|
+
**Cleanup:** delete the created doc
|
|
1951
|
+
|
|
1952
|
+
**Result (2026-07-18) ✅ PASS**
|
|
1953
|
+
2 matches returned. `start_index`/`end_index` (1-6, 21-26) confirmed exact against `get_doc_structure` paragraph offsets (paragraphs start at 1 and 13).
|
|
1954
|
+
|
|
1955
|
+
---
|
|
1956
|
+
|
|
1957
|
+
### TC-DOC112: `find_in_doc` regex search feeds directly into `style_doc_range` to hyperlink matches ⚠️ destructive
|
|
1958
|
+
**Setup:** create a doc via `write_doc_content` with content `<p>Contact test@example.com or admin@example.com for help</p>`
|
|
1959
|
+
|
|
1960
|
+
**Prompt**
|
|
1961
|
+
> "Find every email address in doc {DOC_ID} using a regex, then turn each one into a mailto: link"
|
|
1962
|
+
|
|
1963
|
+
**Checks**
|
|
1964
|
+
- `find_in_doc(doc_id, query=r"[\w.]+@[\w.]+", regex=True)` returns 2 matches with the correct `matched_text` values and offsets
|
|
1965
|
+
- Calling `style_doc_range` with `link_url="mailto:" + matched_text` at each returned `start_index`/`end_index` succeeds
|
|
1966
|
+
- `get_doc_structure` afterward shows both runs with `link_url` set to the corresponding `mailto:` address, confirming the offsets from `find_in_doc` landed on the exact right characters
|
|
1967
|
+
|
|
1968
|
+
**Cleanup:** delete the created doc
|
|
1969
|
+
|
|
1970
|
+
**Result (2026-07-18) ✅ PASS**
|
|
1971
|
+
2 email matches found; `style_doc_range` applied `mailto:` links at the returned offsets; `get_doc_structure` afterward showed both runs split exactly at the email boundaries with the correct `link_url`.
|
|
1972
|
+
|
|
1973
|
+
---
|
|
1974
|
+
|
|
1975
|
+
### TC-DOC113: `find_in_doc` case_sensitive=True excludes different-case matches
|
|
1976
|
+
**Setup:** create a doc via `write_doc_content` with content `<p>Hello World, another hello here</p>`
|
|
1977
|
+
|
|
1978
|
+
**Prompt**
|
|
1979
|
+
> "Find case-sensitive matches of 'hello' in doc {DOC_ID}"
|
|
1980
|
+
|
|
1981
|
+
**Checks**
|
|
1982
|
+
- Returns exactly 1 match (`matched_text: "hello"`) — the capitalized "Hello" in the same doc is excluded
|
|
1983
|
+
|
|
1984
|
+
**Cleanup:** delete the created doc
|
|
1985
|
+
|
|
1986
|
+
**Result (2026-07-18) ✅ PASS**
|
|
1987
|
+
Exactly 1 match returned (`"hello"`); capitalized "Hello" correctly excluded.
|
|
1988
|
+
|
|
1989
|
+
---
|
|
1990
|
+
|
|
1991
|
+
### TC-DOC114: `find_in_doc` invalid regex returned gracefully
|
|
1992
|
+
**Setup:** create a doc via `write_doc_content` with any content
|
|
1993
|
+
|
|
1994
|
+
**Prompt**
|
|
1995
|
+
> "Search doc {DOC_ID} using the regex '(unclosed'"
|
|
1996
|
+
|
|
1997
|
+
**Checks**
|
|
1998
|
+
- Returns `{"error": "..."}` referencing the invalid regex — does not raise an exception
|
|
1999
|
+
|
|
2000
|
+
**Cleanup:** delete the created doc
|
|
2001
|
+
|
|
2002
|
+
**Result (2026-07-18) ✅ PASS**
|
|
2003
|
+
Returned `{"error": "Invalid regex: missing ), unterminated subpattern at position 0"}}` — clean error dict, no exception. Also spot-checked live: an invalid `doc_id` now returns `{"error": "<HttpError 404 ...>"}` instead of raising (regression test for the fix to the missing-try/except finding from code review).
|
|
2004
|
+
|
|
2005
|
+
---
|
|
2006
|
+
|
|
2007
|
+
### TC-DOC115: `find_in_doc` searches table cell text
|
|
2008
|
+
**Setup:** create a doc, insert a 1x1 table via `insert_doc_table`, then write "needle" into the cell via `insert_doc_text` at the cell's `paragraphStartIndex` (from `get_doc_structure`)
|
|
2009
|
+
|
|
2010
|
+
**Prompt**
|
|
2011
|
+
> "Find 'needle' in doc {DOC_ID}"
|
|
2012
|
+
|
|
2013
|
+
**Checks**
|
|
2014
|
+
- Returns 1 match with `matched_text: "needle"` and `start_index` equal to the cell's `paragraphStartIndex`
|
|
2015
|
+
|
|
2016
|
+
**Cleanup:** delete the created doc
|
|
2017
|
+
|
|
2018
|
+
**Result (2026-07-18) ✅ PASS**
|
|
2019
|
+
1 match returned, `start_index` (11) equal to the cell's `paragraphStartIndex` (11).
|
|
@@ -98,7 +98,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
98
98
|
**Defects** _(found after v0.8.1 shipped; too late for that release, don't warrant a standalone patch)_
|
|
99
99
|
- [x] Dev prereleases become unreachable once the base version's stable release ships — `format-jinja` never bumps past the last tag, so `X.Y.Z.devN` is a PEP 440 pre-release of the already-published `X.Y.Z` and always loses precedence to it; fix is `bump = true` in `[tool.uv-dynamic-versioning]` (PR #318) ([#317](https://github.com/khuisman/mcp-gee-sweet/issues/317))
|
|
100
100
|
- [x] `sync_folder` doesn't recurse into subfolders — one-level-only Drive query silently reports a clean "in sync" result while ignoring every nested file (confirmed live: 225 files across 22 subfolders ignored, only the root-level file seen) (PR #328) ([#315](https://github.com/khuisman/mcp-gee-sweet/issues/315))
|
|
101
|
-
- [
|
|
101
|
+
- [x] `download_folder`'s file loop is genuinely sequential (`for f in ...: await asyncio.to_thread(...)`) — the one multi-file transfer path missed when the `asyncio.gather` pattern was established (#183); `sync_folder`'s own transfer step already uses it correctly (PR #351) ([#316](https://github.com/khuisman/mcp-gee-sweet/issues/316))
|
|
102
102
|
- [ ] `sync_folder` re-uploads every file after its first download — download never sets the local file's mtime to match Drive's `modifiedTime`, so any real time gap past the 5s tolerance makes the local copy look "locally newer" on the next sync; confirmed live during #315/#328 QA, reproduces on both new and long-standing files, unrelated to recursion ([#346](https://github.com/khuisman/mcp-gee-sweet/issues/346))
|
|
103
103
|
- [x] Bare URLs in markdown content aren't autolinked — Python-Markdown's built-in autolink only fires on `<https://...>` or `[text](url)`, not a bare URL (PR #265) ([#248](https://github.com/khuisman/mcp-gee-sweet/issues/248))
|
|
104
104
|
- [x] `zip(doc_tables, ast_tables)` in `emitter.py` silently cross-pairs tables when one is skipped (zero-row/zero-col table), misapplying fill/merge/style requests to the wrong table — surfaced during #276's review (PR #282) ([#277](https://github.com/khuisman/mcp-gee-sweet/issues/277))
|
|
@@ -209,7 +209,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
209
209
|
- [ ] Watch notifications — webhook push on calendar or event changes ([#165](https://github.com/khuisman/mcp-gee-sweet/issues/165))
|
|
210
210
|
|
|
211
211
|
**Infrastructure**
|
|
212
|
-
- [
|
|
212
|
+
- [x] `notifications/progress` feedback for long-running transfer calls (`download_folder`, `sync_folder`) — no tool uses MCP progress reporting today, would establish a new pattern; split from #316 (PR #351) ([#319](https://github.com/khuisman/mcp-gee-sweet/issues/319))
|
|
213
213
|
|
|
214
214
|
### Decisions Needed — blocking backlog _(no assigned version)_
|
|
215
215
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Auto-generated by `scripts/gen_tool_docs.py` — do not edit by hand, your changes will be overwritten on the next commit. -->
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
109 tools. All tool names are lowercase with underscores — use these exact strings with `ENABLED_TOOLS` or `--include-tools`.
|
|
6
6
|
|
|
7
7
|
## Sheets — data
|
|
8
8
|
|
|
@@ -125,6 +125,7 @@ Requires the `drive.activity.readonly` scope; uses the Drive Activity API v2.
|
|
|
125
125
|
| `get_doc_content` | Get the plain text content of a Google Doc. | `file_id`, `local_path?` |
|
|
126
126
|
| `write_doc_content` | Replace the full content of an existing Google Doc. | `doc_id`, `content`, `content_format?`, `autolink_urls?` |
|
|
127
127
|
| `get_doc_structure` | Return the full structural map of a Google Doc body with element indices. | `doc_id` |
|
|
128
|
+
| `find_in_doc` | Search a Google Doc's text and return match locations as document character offsets. | `doc_id`, `query`, `regex?`, `case_sensitive?`, `max_results?`, `local_path?` |
|
|
128
129
|
| `insert_doc_text` | Insert text at one or more positions in a Google Doc. | `doc_id`, `insertions` |
|
|
129
130
|
| `delete_doc_range` | Delete one or more content ranges from a Google Doc. | `doc_id`, `deletions` |
|
|
130
131
|
| `create_named_range` | Create a named range over a span of content in a Google Doc. | `doc_id`, `name`, `start_index`, `end_index` |
|
{mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/content.py
RENAMED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import html as html_module
|
|
2
2
|
import logging
|
|
3
|
+
import re
|
|
3
4
|
import xml.etree.ElementTree as etree
|
|
5
|
+
from collections.abc import Iterator
|
|
4
6
|
from html.parser import HTMLParser
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from typing import Any
|
|
@@ -143,6 +145,62 @@ def _html_to_doc_requests(
|
|
|
143
145
|
return _to_doc_requests(html_content, "html", start_index)
|
|
144
146
|
|
|
145
147
|
|
|
148
|
+
def _utf16_units(ch: str) -> int:
|
|
149
|
+
"""UTF-16 code units a single Python character occupies. Docs API indices
|
|
150
|
+
(startIndex/endIndex) are UTF-16 code units, not Python code points — an
|
|
151
|
+
astral-plane character (most emoji, some CJK/math symbols) is one Python
|
|
152
|
+
str character but 2 UTF-16 units (a surrogate pair), so every offset past
|
|
153
|
+
one would drift by 1 if counted with plain enumerate()."""
|
|
154
|
+
return 2 if ord(ch) > 0xFFFF else 1
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _collect_doc_paragraphs(content: list[dict[str, Any]]) -> Iterator[tuple[str, list[int]]]:
|
|
158
|
+
"""Walk document body content, recursing into table cells, yielding each
|
|
159
|
+
paragraph's text paired with a parallel list of document character indices
|
|
160
|
+
(one per Python character in the text, in UTF-16 code units) — lets a
|
|
161
|
+
match's in-paragraph span be translated back into document offsets usable
|
|
162
|
+
with style_doc_range.
|
|
163
|
+
|
|
164
|
+
A generator so a caller (e.g. find_in_doc bounding results by max_results)
|
|
165
|
+
can stop pulling early without walking the rest of a large document.
|
|
166
|
+
|
|
167
|
+
Each ParagraphElement carries its own startIndex, but the Docs API doesn't
|
|
168
|
+
always populate it (observed on a document's very first element). When
|
|
169
|
+
present it's trusted directly, resyncing the running offset; when absent,
|
|
170
|
+
the offset just carries forward from the paragraph's own startIndex plus
|
|
171
|
+
whatever's been consumed so far, so one missing field doesn't silently
|
|
172
|
+
drop that element's text the way an unconditional skip would."""
|
|
173
|
+
for elem in content:
|
|
174
|
+
if "paragraph" in elem:
|
|
175
|
+
# Google Docs body content is never index 0 — a missing startIndex
|
|
176
|
+
# here only happens on the document's very first element, which
|
|
177
|
+
# implicitly starts at 1 (same convention as tables.py/emitter.py).
|
|
178
|
+
offset = elem.get("startIndex", 1)
|
|
179
|
+
text_parts: list[str] = []
|
|
180
|
+
indices: list[int] = []
|
|
181
|
+
for pe in elem["paragraph"].get("elements", []):
|
|
182
|
+
start = pe.get("startIndex")
|
|
183
|
+
if start is not None:
|
|
184
|
+
offset = start
|
|
185
|
+
tr = pe.get("textRun")
|
|
186
|
+
if tr and tr.get("content"):
|
|
187
|
+
run_text = tr["content"]
|
|
188
|
+
text_parts.append(run_text)
|
|
189
|
+
for ch in run_text:
|
|
190
|
+
indices.append(offset)
|
|
191
|
+
offset += _utf16_units(ch)
|
|
192
|
+
else:
|
|
193
|
+
end = pe.get("endIndex")
|
|
194
|
+
if end is not None:
|
|
195
|
+
offset = end
|
|
196
|
+
if text_parts:
|
|
197
|
+
yield "".join(text_parts), indices
|
|
198
|
+
elif "table" in elem:
|
|
199
|
+
for row in elem["table"].get("tableRows", []):
|
|
200
|
+
for cell in row.get("tableCells", []):
|
|
201
|
+
yield from _collect_doc_paragraphs(cell.get("content", []))
|
|
202
|
+
|
|
203
|
+
|
|
146
204
|
async def _create_named_range(
|
|
147
205
|
docs_service, doc_cache, doc_id: str, name: str, start_index: int, end_index: int
|
|
148
206
|
) -> dict[str, Any]:
|
|
@@ -690,6 +748,109 @@ def register(tool):
|
|
|
690
748
|
"elements": elements,
|
|
691
749
|
}
|
|
692
750
|
|
|
751
|
+
@tool(annotations=ToolAnnotations(title="Find in Document", readOnlyHint=True))
|
|
752
|
+
async def find_in_doc(
|
|
753
|
+
doc_id: str,
|
|
754
|
+
query: str,
|
|
755
|
+
regex: bool = False,
|
|
756
|
+
case_sensitive: bool = False,
|
|
757
|
+
max_results: int = 50,
|
|
758
|
+
local_path: str | None = None,
|
|
759
|
+
ctx: Context = None,
|
|
760
|
+
) -> list[dict[str, Any]] | dict[str, Any]:
|
|
761
|
+
"""
|
|
762
|
+
Search a Google Doc's text and return match locations as document
|
|
763
|
+
character offsets.
|
|
764
|
+
|
|
765
|
+
Matches feed directly into style_doc_range's ranges — e.g. to link up
|
|
766
|
+
bare URLs already sitting in a doc: find_in_doc(doc_id,
|
|
767
|
+
query=r'https?://[^\\s<>"]+(?<![.,;:!?)])', regex=True), then
|
|
768
|
+
style_doc_range with link_url per match. The trailing negative
|
|
769
|
+
lookbehind keeps a URL followed by sentence punctuation (or an
|
|
770
|
+
unmatched closing paren) from swallowing it into the link.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
doc_id: The Google Doc file ID.
|
|
774
|
+
query: Text to search for. Interpreted literally by default; pass
|
|
775
|
+
regex=True to treat it as a Python regular expression.
|
|
776
|
+
regex: Whether query is a regular expression (default False).
|
|
777
|
+
case_sensitive: Whether the search is case-sensitive (default False).
|
|
778
|
+
max_results: Maximum number of matches to return (default 50).
|
|
779
|
+
local_path: Optional local filesystem path (file or directory) to write the
|
|
780
|
+
result to instead of returning it inline. Bypasses the response-size cap.
|
|
781
|
+
Same caveat as download_file/download_folder: this path is resolved on the
|
|
782
|
+
*server's* filesystem, not the caller's.
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
List of matches, each with start_index/end_index (document offsets
|
|
786
|
+
usable directly as a style_doc_range range), matched_text, and
|
|
787
|
+
context (the containing paragraph's full text). Searches paragraph
|
|
788
|
+
text in the document body, including table cells; headers,
|
|
789
|
+
footers, and footnotes are not searched. A zero-length regex match
|
|
790
|
+
(e.g. "x*" with no "x" present) is skipped. Returns {"error": ...}
|
|
791
|
+
if query is an invalid regex, or if the Docs API call itself fails
|
|
792
|
+
(e.g. doc_id doesn't exist). max_results bounds match count, not
|
|
793
|
+
response size — matched context can still be large. Raises
|
|
794
|
+
ValueError if the response exceeds a safety cap (default 40,000
|
|
795
|
+
characters, set MAX_TOOL_RESPONSE_CHARS to change it) and
|
|
796
|
+
local_path is not set — lower max_results, or pass local_path. If
|
|
797
|
+
local_path is set, returns {local_path, doc_id, query, match_count,
|
|
798
|
+
bytes_written} instead.
|
|
799
|
+
"""
|
|
800
|
+
lc = ctx.request_context.lifespan_context
|
|
801
|
+
docs_service = lc.docs_service
|
|
802
|
+
|
|
803
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
804
|
+
try:
|
|
805
|
+
pattern = re.compile(query, flags) if regex else re.compile(re.escape(query), flags)
|
|
806
|
+
except re.error as e:
|
|
807
|
+
return {"error": f"Invalid regex: {e}"}
|
|
808
|
+
|
|
809
|
+
try:
|
|
810
|
+
doc = await execute_in_thread(
|
|
811
|
+
docs_service.documents().get(documentId=doc_id).execute,
|
|
812
|
+
docs_service,
|
|
813
|
+
)
|
|
814
|
+
except Exception as e:
|
|
815
|
+
return {"error": str(e)}
|
|
816
|
+
|
|
817
|
+
results: list[dict[str, Any]] = []
|
|
818
|
+
for para_text, para_indices in _collect_doc_paragraphs(
|
|
819
|
+
doc.get("body", {}).get("content", [])
|
|
820
|
+
):
|
|
821
|
+
if len(results) >= max_results:
|
|
822
|
+
break
|
|
823
|
+
for m in pattern.finditer(para_text):
|
|
824
|
+
if len(results) >= max_results:
|
|
825
|
+
break
|
|
826
|
+
start_char, end_char = m.start(), m.end()
|
|
827
|
+
if start_char == end_char:
|
|
828
|
+
continue
|
|
829
|
+
results.append(
|
|
830
|
+
{
|
|
831
|
+
"start_index": para_indices[start_char],
|
|
832
|
+
"end_index": para_indices[end_char - 1]
|
|
833
|
+
+ _utf16_units(para_text[end_char - 1]),
|
|
834
|
+
"matched_text": para_text[start_char:end_char],
|
|
835
|
+
"context": para_text.rstrip("\n"),
|
|
836
|
+
}
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
if local_path:
|
|
840
|
+
return await write_capped_result_to_disk(
|
|
841
|
+
results,
|
|
842
|
+
local_path,
|
|
843
|
+
default_filename="find_in_doc_results.json",
|
|
844
|
+
manifest_extra={
|
|
845
|
+
"doc_id": doc_id,
|
|
846
|
+
"query": query,
|
|
847
|
+
"match_count": len(results),
|
|
848
|
+
},
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
enforce_response_size_cap(results, tool_name="find_in_doc")
|
|
852
|
+
return results
|
|
853
|
+
|
|
693
854
|
@tool(annotations=ToolAnnotations(title="Insert Document Text", destructiveHint=True))
|
|
694
855
|
async def insert_doc_text(
|
|
695
856
|
doc_id: str,
|