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.
Files changed (165) hide show
  1. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CLAUDE.md +1 -1
  2. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/PKG-INFO +1 -1
  3. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/configuration.md +2 -2
  4. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/docs.md +85 -0
  5. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/roadmap.md +2 -2
  6. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/tools.md +2 -1
  7. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/scripts/gen_tool_docs.py +1 -0
  8. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/content.py +161 -0
  9. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_content.py +341 -0
  10. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/cleanup-worktrees.md +0 -0
  11. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/merge-pr.md +0 -0
  12. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/orchestrator.md +0 -0
  13. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/prep-for-pr.md +0 -0
  14. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/release.md +0 -0
  15. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/retro.md +0 -0
  16. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/start-worktree.md +0 -0
  17. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/team-member.md +0 -0
  18. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/commands/verify-pr.md +0 -0
  19. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/settings.json +0 -0
  20. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/amy.md +0 -0
  21. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/aziz.md +0 -0
  22. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/dev.md +0 -0
  23. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/kai.md +0 -0
  24. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.claude/team-roles/qa.md +0 -0
  25. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.dockerignore +0 -0
  26. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  27. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  28. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/dependabot.yml +0 -0
  30. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/ci.yml +0 -0
  31. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/docs.yml +0 -0
  32. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/publish-dev.yml +0 -0
  33. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.github/workflows/release.yml +0 -0
  34. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.gitignore +0 -0
  35. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.pre-commit-config.yaml +0 -0
  36. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/.python-version +0 -0
  37. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CHANGELOG.md +0 -0
  38. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CODE_OF_CONDUCT.md +0 -0
  39. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/CONTRIBUTING.md +0 -0
  40. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/Dockerfile +0 -0
  41. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/LICENSE +0 -0
  42. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/Makefile +0 -0
  43. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/README.md +0 -0
  44. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/SECURITY.md +0 -0
  45. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docker-compose.yml +0 -0
  46. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/auth.md +0 -0
  47. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/client-setup.md +0 -0
  48. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-async-tool-execution.md +0 -0
  49. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-cache-invalidation.md +0 -0
  50. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-chart-theming.md +0 -0
  51. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-composite-tools.md +0 -0
  52. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-dev-team.md +0 -0
  53. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-docs-formatting.md +0 -0
  54. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-fork.md +0 -0
  55. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  56. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-publishing.md +0 -0
  57. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-release-docs-roles.md +0 -0
  58. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-repositioning.md +0 -0
  59. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  60. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  61. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/decision-testing.md +0 -0
  62. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/decisions/index.md +0 -0
  63. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/docs-ast-pipeline.md +0 -0
  64. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/effectiveformat-spike.md +0 -0
  65. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/index.md +0 -0
  66. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/markdown-support.md +0 -0
  67. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design/phase3-theme-system.md +0 -0
  68. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/design.md +0 -0
  69. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/index.md +0 -0
  70. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/known-limitations.md +0 -0
  71. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/notes-read.md +0 -0
  72. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/README.md +0 -0
  73. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  74. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  75. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  76. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  77. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  78. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  79. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  80. {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
  81. {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
  82. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/fixtures.template.md +0 -0
  83. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/operations.yaml +0 -0
  84. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/playwright_oauth.md +0 -0
  85. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/.gitkeep +0 -0
  86. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-02.md +0 -0
  87. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-14.md +0 -0
  88. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-17.md +0 -0
  89. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-18.md +0 -0
  90. {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
  91. {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
  92. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-06-28.md +0 -0
  93. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04-docs.md +0 -0
  94. {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
  95. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04-infra.md +0 -0
  96. {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
  97. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/results/2026-07-04.md +0 -0
  98. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/retro-v0.8.0.md +0 -0
  99. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/run.md +0 -0
  100. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/runs/README.md +0 -0
  101. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/runs/v0.8.0.md +0 -0
  102. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/runs/v0.8.1.md +0 -0
  103. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/setup.md +0 -0
  104. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/calendar.md +0 -0
  105. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/drive.md +0 -0
  106. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/infra.md +0 -0
  107. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_charts.md +0 -0
  108. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_mgmt.md +0 -0
  109. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_read.md +0 -0
  110. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa/tests/sheets_write.md +0 -0
  111. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/docs/qa-checklist.md +0 -0
  112. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/mkdocs.yml +0 -0
  113. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/pyproject.toml +0 -0
  114. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/pyrightconfig.json +0 -0
  115. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/scripts/oauth_setup.py +0 -0
  116. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/scripts/setup_team.sh +0 -0
  117. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/.env.template +0 -0
  118. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/__init__.py +0 -0
  119. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/auth.py +0 -0
  120. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/cache.py +0 -0
  121. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/http_transport.py +0 -0
  122. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/server.py +0 -0
  123. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  124. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/cache.py +0 -0
  125. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  126. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  127. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  128. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  129. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  130. {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
  131. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  132. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  133. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  134. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  135. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  136. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  137. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  138. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  139. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  140. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  141. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  142. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  143. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  144. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/__init__.py +0 -0
  145. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/__init__.py +0 -0
  146. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_activity.py +0 -0
  147. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_files.py +0 -0
  148. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_sharing.py +0 -0
  149. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/drive/test_transfer.py +0 -0
  150. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/__init__.py +0 -0
  151. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/test_data.py +0 -0
  152. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/test_helpers.py +0 -0
  153. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/sheets/test_structure.py +0 -0
  154. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_auth.py +0 -0
  155. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_cache.py +0 -0
  156. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_calendar.py +0 -0
  157. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_comments.py +0 -0
  158. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_core.py +0 -0
  159. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_layout.py +0 -0
  160. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_style.py +0 -0
  161. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_docs_tables.py +0 -0
  162. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_gen_tool_docs.py +0 -0
  163. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_response_limits.py +0 -0
  164. {mcp_gee_sweet-0.8.2.dev71 → mcp_gee_sweet-0.8.2.dev73}/tests/test_server.py +0 -0
  165. {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.dev71
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 108 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.
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
- - [ ] `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 ([#316](https://github.com/khuisman/mcp-gee-sweet/issues/316))
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
- - [ ] `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 ([#319](https://github.com/khuisman/mcp-gee-sweet/issues/319))
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
- 108 tools. All tool names are lowercase with underscores — use these exact strings with `ENABLED_TOOLS` or `--include-tools`.
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` |
@@ -215,6 +215,7 @@ SUBSETS = [
215
215
  "create_doc",
216
216
  "get_doc_content",
217
217
  "get_doc_structure",
218
+ "find_in_doc",
218
219
  "write_doc_content",
219
220
  "insert_doc_text",
220
221
  "insert_doc_table",
@@ -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,