mcp-gee-sweet 0.8.2.dev45__tar.gz → 0.8.2.dev47__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 (156) hide show
  1. mcp_gee_sweet-0.8.2.dev47/.claude/settings.json +5 -0
  2. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/CLAUDE.md +1 -1
  3. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/PKG-INFO +3 -3
  4. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/README.md +2 -2
  5. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/configuration.md +1 -1
  6. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/run.md +1 -0
  7. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/sheets_mgmt.md +168 -0
  8. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/roadmap.md +1 -1
  9. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/tools.md +3 -1
  10. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/sheets/structure.py +169 -0
  11. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/sheets/test_structure.py +159 -0
  12. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/cleanup-worktrees.md +0 -0
  13. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/merge-pr.md +0 -0
  14. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/orchestrator.md +0 -0
  15. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/prep-for-pr.md +0 -0
  16. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/release.md +0 -0
  17. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/start-worktree.md +0 -0
  18. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/team-member.md +0 -0
  19. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/commands/verify-pr.md +0 -0
  20. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/team-roles/amy.md +0 -0
  21. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/team-roles/aziz.md +0 -0
  22. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/team-roles/dev.md +0 -0
  23. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/team-roles/kai.md +0 -0
  24. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.claude/team-roles/qa.md +0 -0
  25. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.dockerignore +0 -0
  26. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  27. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  28. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/dependabot.yml +0 -0
  30. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/workflows/ci.yml +0 -0
  31. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/workflows/docs.yml +0 -0
  32. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/workflows/publish-dev.yml +0 -0
  33. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.github/workflows/release.yml +0 -0
  34. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.gitignore +0 -0
  35. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.pre-commit-config.yaml +0 -0
  36. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/.python-version +0 -0
  37. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/CHANGELOG.md +0 -0
  38. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/CODE_OF_CONDUCT.md +0 -0
  39. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/CONTRIBUTING.md +0 -0
  40. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/Dockerfile +0 -0
  41. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/LICENSE +0 -0
  42. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/Makefile +0 -0
  43. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/SECURITY.md +0 -0
  44. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docker-compose.yml +0 -0
  45. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/auth.md +0 -0
  46. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/client-setup.md +0 -0
  47. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-async-tool-execution.md +0 -0
  48. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-cache-invalidation.md +0 -0
  49. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-chart-theming.md +0 -0
  50. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-composite-tools.md +0 -0
  51. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-dev-team.md +0 -0
  52. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-docs-formatting.md +0 -0
  53. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-fork.md +0 -0
  54. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  55. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-publishing.md +0 -0
  56. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-release-docs-roles.md +0 -0
  57. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-repositioning.md +0 -0
  58. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  59. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/decision-testing.md +0 -0
  60. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/decisions/index.md +0 -0
  61. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/design/docs-ast-pipeline.md +0 -0
  62. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/design/effectiveformat-spike.md +0 -0
  63. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/design/index.md +0 -0
  64. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/design/markdown-support.md +0 -0
  65. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/design/phase3-theme-system.md +0 -0
  66. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/design.md +0 -0
  67. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/index.md +0 -0
  68. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/known-limitations.md +0 -0
  69. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/notes-read.md +0 -0
  70. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/README.md +0 -0
  71. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  72. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  73. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  74. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  75. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/fixtures.template.md +0 -0
  76. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/operations.yaml +0 -0
  77. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/playwright_oauth.md +0 -0
  78. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/.gitkeep +0 -0
  79. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-02.md +0 -0
  80. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-14.md +0 -0
  81. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-17.md +0 -0
  82. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-18.md +0 -0
  83. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  84. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  85. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-06-28.md +0 -0
  86. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-07-04-docs.md +0 -0
  87. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  88. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-07-04-infra.md +0 -0
  89. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  90. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/results/2026-07-04.md +0 -0
  91. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/retro-v0.8.0.md +0 -0
  92. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/runs/README.md +0 -0
  93. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/runs/v0.8.0.md +0 -0
  94. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/runs/v0.8.1.md +0 -0
  95. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/setup.md +0 -0
  96. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/calendar.md +0 -0
  97. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/docs.md +0 -0
  98. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/drive.md +0 -0
  99. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/infra.md +0 -0
  100. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/sheets_charts.md +0 -0
  101. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/sheets_read.md +0 -0
  102. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa/tests/sheets_write.md +0 -0
  103. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/docs/qa-checklist.md +0 -0
  104. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/mkdocs.yml +0 -0
  105. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/pyproject.toml +0 -0
  106. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/pyrightconfig.json +0 -0
  107. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/scripts/gen_tool_docs.py +0 -0
  108. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/scripts/oauth_setup.py +0 -0
  109. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/scripts/setup_team.sh +0 -0
  110. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/.env.template +0 -0
  111. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/__init__.py +0 -0
  112. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/auth.py +0 -0
  113. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/cache.py +0 -0
  114. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/http_transport.py +0 -0
  115. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/server.py +0 -0
  116. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  117. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/cache.py +0 -0
  118. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  119. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  120. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  121. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  122. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  123. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  124. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  125. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  126. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  127. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  128. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  129. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  130. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  131. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  132. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  133. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  134. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  135. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  136. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/__init__.py +0 -0
  137. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/drive/__init__.py +0 -0
  138. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/drive/test_activity.py +0 -0
  139. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/drive/test_files.py +0 -0
  140. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/drive/test_sharing.py +0 -0
  141. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/drive/test_transfer.py +0 -0
  142. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/sheets/__init__.py +0 -0
  143. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/sheets/test_data.py +0 -0
  144. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/sheets/test_helpers.py +0 -0
  145. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_auth.py +0 -0
  146. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_cache.py +0 -0
  147. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_calendar.py +0 -0
  148. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_docs_content.py +0 -0
  149. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_docs_core.py +0 -0
  150. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_docs_layout.py +0 -0
  151. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_docs_style.py +0 -0
  152. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_docs_tables.py +0 -0
  153. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_gen_tool_docs.py +0 -0
  154. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_response_limits.py +0 -0
  155. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/tests/test_server.py +0 -0
  156. {mcp_gee_sweet-0.8.2.dev45 → mcp_gee_sweet-0.8.2.dev47}/uv.lock +0 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "worktree": {
3
+ "bgIsolation": "none"
4
+ }
5
+ }
@@ -32,7 +32,7 @@ Tests live in `tests/` and can be run with `uv run pytest`.
32
32
  Logic is split across `src/mcp_gee_sweet/`: `server.py` (MCP setup, tool decorator, resources), `auth.py` (lifespan, `SpreadsheetContext`), and `tools/` (domain-based layout):
33
33
 
34
34
  - `tools/sheets/data.py` — read/write cell data (`get_sheet_data`, `get_sheet_formulas`, `get_multiple_*`, `find_in_spreadsheet`, `clear_values`, `update_cells`, `batch_update_cells`, `batch_update`)
35
- - `tools/sheets/structure.py` — sheet structure (`list_sheets`, `copy_sheet`, `duplicate_sheet`, `rename_sheet`, `create_sheet`, `delete_sheet`, `add_rows`, `add_columns`, `delete_rows`, `delete_columns`, `hide_rows`, `unhide_rows`, `hide_columns`, `unhide_columns`, `format_cells`, `merge_cells`, `unmerge_cells`, `freeze`, `update_sheet_properties`, `sort_range`, `add_chart`)
35
+ - `tools/sheets/structure.py` — sheet structure (`list_sheets`, `copy_sheet`, `duplicate_sheet`, `rename_sheet`, `create_sheet`, `delete_sheet`, `add_rows`, `add_columns`, `delete_rows`, `delete_columns`, `hide_rows`, `unhide_rows`, `hide_columns`, `unhide_columns`, `resize_rows`, `resize_columns`, `format_cells`, `merge_cells`, `unmerge_cells`, `freeze`, `update_sheet_properties`, `sort_range`, `add_chart`)
36
36
  - `tools/sheets/helpers.py` — A1 notation helpers (`_parse_a1_notation`, `_column_index_to_letter`, etc.)
37
37
  - `tools/drive/files.py` — file/folder operations (`create_spreadsheet`, `import_csv_to_sheet`, `list_spreadsheets`, `list_files`, `search_files`, `create_folder`, `copy_file`, `move_file`, `rename_file`, `delete_file`, `list_shared_with_me`, `list_recent_files`, `get_storage_quota`, etc.)
38
38
  - `tools/drive/sharing.py` — permissions (`share_spreadsheet`, `share_file`, `list_permissions`, `update_permission`, `remove_permission`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-gee-sweet
3
- Version: 0.8.2.dev45
3
+ Version: 0.8.2.dev47
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
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
27
27
  ![GitHub License](https://img.shields.io/github/license/khuisman/mcp-gee-sweet)
28
28
  </div>
29
29
 
30
- An MCP server that gives AI clients reliable, direct access to Google Workspace — Sheets, Drive, Docs, and Calendar. 90 tools across four domains.
30
+ An MCP server that gives AI clients reliable, direct access to Google Workspace — Sheets, Drive, Docs, and Calendar. 101 tools across four domains.
31
31
 
32
32
  As of mid-2026, Google's own Workspace MCP servers (Developer Preview) cover Gmail, Drive, Calendar, People, and Chat — Sheets and Docs still have no official server at all, and Drive/Calendar coverage there is a fraction of what's here. Stable releases ship to PyPI behind a QA regression gate, with a raw `batch_update` passthrough as an escape hatch for anything a named tool doesn't cover yet. See [Why it exists](https://khuisman.github.io/mcp-gee-sweet/#why-it-exists) for the full, sourced comparison against both the official servers and community alternatives.
33
33
 
@@ -124,7 +124,7 @@ See [Client Setup](https://khuisman.github.io/mcp-gee-sweet/latest/client-setup/
124
124
 
125
125
  ## Docs
126
126
 
127
- - [Tools](https://khuisman.github.io/mcp-gee-sweet/latest/tools/) — full tool reference (90 tools)
127
+ - [Tools](https://khuisman.github.io/mcp-gee-sweet/latest/tools/) — full tool reference (101 tools)
128
128
  - [Authentication](https://khuisman.github.io/mcp-gee-sweet/latest/auth/) — all four auth methods
129
129
  - [Configuration](https://khuisman.github.io/mcp-gee-sweet/latest/configuration/) — env vars, caching, tool filtering
130
130
  - [Client Setup](https://khuisman.github.io/mcp-gee-sweet/latest/client-setup/) — MCP client config examples
@@ -5,7 +5,7 @@
5
5
  ![GitHub License](https://img.shields.io/github/license/khuisman/mcp-gee-sweet)
6
6
  </div>
7
7
 
8
- An MCP server that gives AI clients reliable, direct access to Google Workspace — Sheets, Drive, Docs, and Calendar. 90 tools across four domains.
8
+ An MCP server that gives AI clients reliable, direct access to Google Workspace — Sheets, Drive, Docs, and Calendar. 101 tools across four domains.
9
9
 
10
10
  As of mid-2026, Google's own Workspace MCP servers (Developer Preview) cover Gmail, Drive, Calendar, People, and Chat — Sheets and Docs still have no official server at all, and Drive/Calendar coverage there is a fraction of what's here. Stable releases ship to PyPI behind a QA regression gate, with a raw `batch_update` passthrough as an escape hatch for anything a named tool doesn't cover yet. See [Why it exists](https://khuisman.github.io/mcp-gee-sweet/#why-it-exists) for the full, sourced comparison against both the official servers and community alternatives.
11
11
 
@@ -102,7 +102,7 @@ See [Client Setup](https://khuisman.github.io/mcp-gee-sweet/latest/client-setup/
102
102
 
103
103
  ## Docs
104
104
 
105
- - [Tools](https://khuisman.github.io/mcp-gee-sweet/latest/tools/) — full tool reference (90 tools)
105
+ - [Tools](https://khuisman.github.io/mcp-gee-sweet/latest/tools/) — full tool reference (101 tools)
106
106
  - [Authentication](https://khuisman.github.io/mcp-gee-sweet/latest/auth/) — all four auth methods
107
107
  - [Configuration](https://khuisman.github.io/mcp-gee-sweet/latest/configuration/) — env vars, caching, tool filtering
108
108
  - [Client Setup](https://khuisman.github.io/mcp-gee-sweet/latest/client-setup/) — MCP client config examples
@@ -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 100 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 102 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
@@ -36,6 +36,7 @@ When a test case is tagged `**Playwright: required**`, after the tool call the c
36
36
 
37
37
  - **Footer / header content** — Google Docs renders headers and footers outside the main document canvas. For short documents `window.scrollTo` does not bring the footer into the viewport. When a test involves `create_footer` or `create_header`, use the API response as confirmation; a Playwright snapshot of the body is still useful but will not show the footer content.
38
38
  - **Permission changes** — sharing confirmations are not visible in the Drive UI without navigating to the file's "Share" dialog. Use `list_permissions` API response as the confirmation source for share tests.
39
+ - **Chart-covered grid (Sales sheet)** — repeated `add_chart` test runs leave floating chart objects on the Sales sheet (they're never cleaned up between runs), and as of 2026-07-15 there were ~12 stacked overlapping charts covering roughly rows 1–22. This blocks visual verification of anything in that region — row-height/column-width changes (`resize_rows`/`resize_columns`), cell formatting, etc. are not visible in a screenshot, and Playwright's accessibility snapshot doesn't expose per-cell grid geometry either (only the charts' own accessible text). When a test in that area needs precise confirmation, use `get_sheet_data(..., range=<affected range>, include_grid_data=True)` instead — the raw response includes `rowMetadata`/`columnMetadata` with each row/column's actual `pixelSize`, which is more precise than a screenshot anyway. Longer term, `add_chart` QA test cases should delete the chart they create at the end of each run (see `docs/qa/tests/sheets_charts.md`) so the fixture doesn't accumulate; that cleanup is not yet in place.
39
40
 
40
41
  ### Coordinating Playwright across parallel shards
41
42
 
@@ -642,6 +642,174 @@ Tests marked **⚠️ destructive** rename or delete sheets — reset fixtures a
642
642
 
643
643
  ---
644
644
 
645
+ ## `resize_rows` / `resize_columns`
646
+
647
+ ### TC-S73: Resize a single row to an explicit pixel height ⚠️ destructive
648
+
649
+ **Prompt**
650
+ **Playwright: required**
651
+ > "Set row 5 (0-based index 4) on the Sales sheet in {SPREADSHEET_ID} to 60 pixels tall"
652
+
653
+ **Checks**
654
+ - `updateDimensionProperties` request sent with `dimension: ROWS`, `startIndex: 4`, `endIndex: 5`
655
+ - `properties.pixelSize: 60`, `fields: pixelSize`
656
+ - Row 5 visibly taller in the Sheets UI
657
+
658
+ **Result (2026-07-15) ✅ PASS** `resize_rows(spreadsheet_id, sheet="Sales", start_row=4, pixel_size=60)` → `{"replies":[{}]}`, no error. Playwright was skipped for this run — the shared fixture's Sales sheet currently has ~12 overlapping chart objects left over from other QA passes, which visually cover rows 1–22 and make row-height differences unreadable in a screenshot. Verified precisely instead via `get_sheet_data(..., range="A1:E6", include_grid_data=True)`, which returns `rowMetadata[].pixelSize` — confirmed row index 4 read back 60 immediately after this call (before being overwritten by TC-S74/TC-S75 below).
659
+
660
+ ---
661
+
662
+ ### TC-S74: Resize a range of rows ⚠️ destructive
663
+
664
+ **Prompt**
665
+ **Playwright: required**
666
+ > "Set rows 3 through 5 (0-based indices 2–4) on the Sales sheet in {SPREADSHEET_ID} to 40 pixels tall"
667
+
668
+ **Checks**
669
+ - `startIndex: 2`, `endIndex: 5` in the request (inclusive end_row=4 translated to exclusive 5)
670
+ - All three rows resize in the UI
671
+
672
+ **Result (2026-07-15) ✅ PASS** `resize_rows(spreadsheet_id, sheet="Sales", start_row=2, end_row=4, pixel_size=40)` → `{"replies":[{}]}`, no error. Confirmed via `get_sheet_data(..., range="A1:E6", include_grid_data=True)`: `rowMetadata` for row indices 2 and 3 both read back `pixelSize: 40` after the full test sequence (index 4 was subsequently auto-resized by TC-S75, as expected).
673
+
674
+ ---
675
+
676
+ ### TC-S75: Auto-resize rows to fit content ⚠️ destructive
677
+
678
+ **Prompt**
679
+ **Playwright: required**
680
+ > "Auto-fit the height of row 5 (0-based index 4) on the Sales sheet in {SPREADSHEET_ID} to its content"
681
+
682
+ **Setup:** Row 5 previously set to an oversized pixel height (e.g. TC-S73).
683
+
684
+ **Checks**
685
+ - `autoResizeDimensions` request sent with `dimensions.dimension: ROWS`, `startIndex: 4`, `endIndex: 5`
686
+ - Row 5 shrinks back to content-fit height in the Sheets UI
687
+
688
+ **Result (2026-07-15) ✅ PASS** `resize_rows(spreadsheet_id, sheet="Sales", start_row=4, auto_resize=True)` → `{"replies":[{}]}`, no error. Confirmed via `get_sheet_data(..., range="A1:E6", include_grid_data=True)`: row index 4's `pixelSize` read back as `21` (Sheets' default/content-fit height for plain text), down from the `40` set by TC-S74 moments earlier — confirms `autoResizeDimensions` fired and took effect.
689
+
690
+ ---
691
+
692
+ ### TC-S76: Resize rows — neither pixel_size nor auto_resize given returns error
693
+
694
+ **Prompt**
695
+ > "Resize row 0 on the Sales sheet in {SPREADSHEET_ID} without specifying a size or auto-fit"
696
+
697
+ **Checks**
698
+ - Response contains `error` field
699
+ - No `batchUpdate` call made
700
+
701
+ **Result (2026-07-15) ✅ PASS** `resize_rows(spreadsheet_id, sheet="Sales", start_row=0)` → `{"error":"Specify pixel_size or set auto_resize=True"}`. Returned before any batchUpdate call.
702
+
703
+ ---
704
+
705
+ ### TC-S77: Resize rows — both pixel_size and auto_resize given returns error
706
+
707
+ **Prompt**
708
+ > "Resize row 0 on the Sales sheet in {SPREADSHEET_ID} to 50 pixels and also auto-fit it"
709
+
710
+ **Checks**
711
+ - Response contains `error` field
712
+ - No `batchUpdate` call made
713
+
714
+ **Result (2026-07-15) ✅ PASS** `resize_rows(spreadsheet_id, sheet="Sales", start_row=0, pixel_size=50, auto_resize=True)` → `{"error":"Specify only one of pixel_size or auto_resize"}`. Returned before any batchUpdate call.
715
+
716
+ ---
717
+
718
+ ### TC-S78: Resize rows — sheet not found returns error
719
+
720
+ **Prompt**
721
+ > "Set row 0 to 50 pixels tall on a sheet called 'NoSuchSheet' in {SPREADSHEET_ID}"
722
+
723
+ **Checks**
724
+ - Response contains `error` field
725
+
726
+ **Result (2026-07-15) ✅ PASS** `resize_rows(spreadsheet_id, sheet="NoSuchSheet", start_row=0, pixel_size=50)` → `{"error":"Sheet 'NoSuchSheet' not found"}`.
727
+
728
+ ---
729
+
730
+ ### TC-S79: Resize a single column to an explicit pixel width ⚠️ destructive
731
+
732
+ **Prompt**
733
+ **Playwright: required**
734
+ > "Set column B (0-based index 1) on the Sales sheet in {SPREADSHEET_ID} to 200 pixels wide"
735
+
736
+ **Checks**
737
+ - `updateDimensionProperties` request sent with `dimension: COLUMNS`, `startIndex: 1`, `endIndex: 2`
738
+ - `properties.pixelSize: 200`, `fields: pixelSize`
739
+ - Column B visibly wider in the Sheets UI
740
+
741
+ **Result (2026-07-15) ✅ PASS** `resize_columns(spreadsheet_id, sheet="Sales", start_column=1, pixel_size=200)` → `{"replies":[{}]}`, no error. Playwright skipped — see "Chart-covered grid" note in `docs/qa/run.md`. Verified via `get_sheet_data(..., range="A1:E6", include_grid_data=True)`: column index 1's `pixelSize` read back `200` immediately after this call (before being overwritten by TC-S81 below).
742
+
743
+ ---
744
+
745
+ ### TC-S80: Resize a range of columns ⚠️ destructive
746
+
747
+ **Prompt**
748
+ **Playwright: required**
749
+ > "Set columns C through E (0-based indices 2–4) on the Sales sheet in {SPREADSHEET_ID} to 50 pixels wide"
750
+
751
+ **Checks**
752
+ - `startIndex: 2`, `endIndex: 5` in the request
753
+ - All columns in range resize in the UI
754
+
755
+ **Result (2026-07-15) ✅ PASS** `resize_columns(spreadsheet_id, sheet="Sales", start_column=2, end_column=4, pixel_size=50)` → `{"replies":[{}]}`, no error. Confirmed via `get_sheet_data(..., range="A1:E6", include_grid_data=True)`: column indices 2, 3, and 4 all read back `pixelSize: 50`.
756
+
757
+ ---
758
+
759
+ ### TC-S81: Auto-resize columns to fit content ⚠️ destructive
760
+
761
+ **Prompt**
762
+ **Playwright: required**
763
+ > "Auto-fit the width of column B (0-based index 1) on the Sales sheet in {SPREADSHEET_ID} to its content"
764
+
765
+ **Setup:** Column B previously set to an oversized pixel width (e.g. TC-S79).
766
+
767
+ **Checks**
768
+ - `autoResizeDimensions` request sent with `dimensions.dimension: COLUMNS`, `startIndex: 1`, `endIndex: 2`
769
+ - Column B shrinks back to content-fit width in the Sheets UI
770
+
771
+ **Result (2026-07-15) ✅ PASS** `resize_columns(spreadsheet_id, sheet="Sales", start_column=1, auto_resize=True)` → `{"replies":[{}]}`, no error. Confirmed via `get_sheet_data(..., range="A1:E6", include_grid_data=True)`: column index 1's `pixelSize` read back `28` (content-fit for the short numeric values in column B), down from the `200` set by TC-S79 — confirms `autoResizeDimensions` fired and took effect.
772
+
773
+ ---
774
+
775
+ ### TC-S82: Resize columns — neither pixel_size nor auto_resize given returns error
776
+
777
+ **Prompt**
778
+ > "Resize column 0 on the Sales sheet in {SPREADSHEET_ID} without specifying a size or auto-fit"
779
+
780
+ **Checks**
781
+ - Response contains `error` field
782
+ - No `batchUpdate` call made
783
+
784
+ **Result (2026-07-15) ✅ PASS** `resize_columns(spreadsheet_id, sheet="Sales", start_column=0)` → `{"error":"Specify pixel_size or set auto_resize=True"}`. Returned before any batchUpdate call.
785
+
786
+ ---
787
+
788
+ ### TC-S83: Resize columns — both pixel_size and auto_resize given returns error
789
+
790
+ **Prompt**
791
+ > "Resize column 0 on the Sales sheet in {SPREADSHEET_ID} to 100 pixels and also auto-fit it"
792
+
793
+ **Checks**
794
+ - Response contains `error` field
795
+ - No `batchUpdate` call made
796
+
797
+ **Result (2026-07-15) ✅ PASS** `resize_columns(spreadsheet_id, sheet="Sales", start_column=0, pixel_size=100, auto_resize=True)` → `{"error":"Specify only one of pixel_size or auto_resize"}`. Returned before any batchUpdate call.
798
+
799
+ ---
800
+
801
+ ### TC-S84: Resize columns — sheet not found returns error
802
+
803
+ **Prompt**
804
+ > "Set column 0 to 100 pixels wide on a sheet called 'NoSuchSheet' in {SPREADSHEET_ID}"
805
+
806
+ **Checks**
807
+ - Response contains `error` field
808
+
809
+ **Result (2026-07-15) ✅ PASS** `resize_columns(spreadsheet_id, sheet="NoSuchSheet", start_column=0, pixel_size=100)` → `{"error":"Sheet 'NoSuchSheet' not found"}`.
810
+
811
+ ---
812
+
645
813
  ## `format_cells`
646
814
 
647
815
  ### TC-S33: Apply bold and background color to a range ⚠️ destructive
@@ -119,7 +119,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
119
119
  - [ ] `list_all_events` — query all subscribed calendars in parallel _(decision needed — also tracked in Decisions Needed)_ ([#194](https://github.com/khuisman/mcp-gee-sweet/issues/194))
120
120
 
121
121
  **Docs**
122
- - [ ] `insert_page_break` — explicit page break at an index ([#148](https://github.com/khuisman/mcp-gee-sweet/issues/148))
122
+ - [x] `insert_page_break` — explicit page break at an index ([#148](https://github.com/khuisman/mcp-gee-sweet/issues/148)) (PR #314)
123
123
  - [x] `merge_table_cells` — merge cells in an existing table (distinct from write-time colspan) ([#150](https://github.com/khuisman/mcp-gee-sweet/issues/150)) (PR #310)
124
124
  - [ ] Comments API — list, add, resolve doc comments ([#151](https://github.com/khuisman/mcp-gee-sweet/issues/151))
125
125
  - [ ] `create_named_range` / `create_bookmark` — anchor points for internal links ([#152](https://github.com/khuisman/mcp-gee-sweet/issues/152))
@@ -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
- 100 tools. All tool names are lowercase with underscores — use these exact strings with `ENABLED_TOOLS` or `--include-tools`.
5
+ 102 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
 
@@ -38,6 +38,8 @@
38
38
  | `unhide_rows` | Unhide previously hidden rows in a sheet. | `spreadsheet_id`, `sheet`, `start_row`, `end_row?` |
39
39
  | `hide_columns` | Hide columns in a sheet from view. | `spreadsheet_id`, `sheet`, `start_column`, `end_column?` |
40
40
  | `unhide_columns` | Unhide previously hidden columns in a sheet. | `spreadsheet_id`, `sheet`, `start_column`, `end_column?` |
41
+ | `resize_rows` | Set an explicit pixel height for rows, or auto-fit them to content. | `spreadsheet_id`, `sheet`, `start_row`, `end_row?`, `pixel_size?`, `auto_resize?` |
42
+ | `resize_columns` | Set an explicit pixel width for columns, or auto-fit them to content. | `spreadsheet_id`, `sheet`, `start_column`, `end_column?`, `pixel_size?`, `auto_resize?` |
41
43
  | `format_cells` | Apply formatting to a cell range (background color, font, alignment, number format). | `spreadsheet_id`, `sheet`, `range`, `bold?`, `italic?`, `strikethrough?`, `font_size?`, `font_color?`, `background_color?`, `horizontal_alignment?`, `vertical_alignment?`, `number_format_type?`, `number_format_pattern?` |
42
44
  | `merge_cells` | Merge a range of cells into one cell. | `spreadsheet_id`, `sheet`, `range`, `merge_type?` |
43
45
  | `unmerge_cells` | Unmerge all merged cells in a range. | `spreadsheet_id`, `sheet`, `range` |
@@ -67,6 +67,73 @@ async def _set_dimension_hidden(
67
67
  )
68
68
 
69
69
 
70
+ async def _set_dimension_pixel_size(
71
+ sheets_service,
72
+ spreadsheet_id: str,
73
+ sheet_id: int,
74
+ dimension: str,
75
+ start_index: int,
76
+ end_index: int,
77
+ pixel_size: int,
78
+ ) -> dict[str, Any]:
79
+ return await execute_in_thread(
80
+ sheets_service.spreadsheets()
81
+ .batchUpdate(
82
+ spreadsheetId=spreadsheet_id,
83
+ body={
84
+ "requests": [
85
+ {
86
+ "updateDimensionProperties": {
87
+ "range": {
88
+ "sheetId": sheet_id,
89
+ "dimension": dimension,
90
+ "startIndex": start_index,
91
+ "endIndex": end_index,
92
+ },
93
+ "properties": {"pixelSize": pixel_size},
94
+ "fields": "pixelSize",
95
+ }
96
+ }
97
+ ]
98
+ },
99
+ )
100
+ .execute,
101
+ sheets_service,
102
+ )
103
+
104
+
105
+ async def _auto_resize_dimension(
106
+ sheets_service,
107
+ spreadsheet_id: str,
108
+ sheet_id: int,
109
+ dimension: str,
110
+ start_index: int,
111
+ end_index: int,
112
+ ) -> dict[str, Any]:
113
+ return await execute_in_thread(
114
+ sheets_service.spreadsheets()
115
+ .batchUpdate(
116
+ spreadsheetId=spreadsheet_id,
117
+ body={
118
+ "requests": [
119
+ {
120
+ "autoResizeDimensions": {
121
+ "dimensions": {
122
+ "sheetId": sheet_id,
123
+ "dimension": dimension,
124
+ "startIndex": start_index,
125
+ "endIndex": end_index,
126
+ }
127
+ }
128
+ }
129
+ ]
130
+ },
131
+ )
132
+ .execute,
133
+ sheets_service,
134
+ )
135
+
136
+
70
137
  def register(tool):
71
138
  @tool(annotations=ToolAnnotations(title="List Sheets", readOnlyHint=True))
72
139
  async def list_sheets(spreadsheet_id: str, ctx: Context = None) -> list[str]:
@@ -719,6 +786,108 @@ def register(tool):
719
786
  sheets_service, spreadsheet_id, sheet_id, "COLUMNS", start_column, end_index, False
720
787
  )
721
788
 
789
+ @tool(annotations=ToolAnnotations(title="Resize Rows", destructiveHint=True))
790
+ async def resize_rows(
791
+ spreadsheet_id: str,
792
+ sheet: str,
793
+ start_row: int,
794
+ end_row: int | None = None,
795
+ pixel_size: int | None = None,
796
+ auto_resize: bool = False,
797
+ ctx: Context = None,
798
+ ) -> dict[str, Any]:
799
+ """
800
+ Set an explicit pixel height for rows, or auto-fit them to content.
801
+
802
+ Args:
803
+ spreadsheet_id: The ID of the spreadsheet (found in the URL)
804
+ sheet: The name of the sheet
805
+ start_row: 0-based index of the first row to resize
806
+ end_row: 0-based index of the last row to resize (inclusive).
807
+ If omitted, resizes only start_row.
808
+ pixel_size: Explicit row height in pixels. Mutually exclusive with auto_resize.
809
+ auto_resize: If True, auto-fit row height to content instead of setting
810
+ an explicit pixel_size.
811
+
812
+ Returns:
813
+ Result of the batchUpdate operation
814
+ """
815
+ lc = ctx.request_context.lifespan_context
816
+ sheets_service = lc.sheets_service
817
+
818
+ if pixel_size is None and not auto_resize:
819
+ return {"error": "Specify pixel_size or set auto_resize=True"}
820
+ if pixel_size is not None and auto_resize:
821
+ return {"error": "Specify only one of pixel_size or auto_resize"}
822
+
823
+ sheet_id = await _get_sheet_id(
824
+ sheets_service, spreadsheet_id, sheet, lc.cache, lc.drive_service
825
+ )
826
+ if sheet_id is None:
827
+ return {"error": f"Sheet '{sheet}' not found"}
828
+
829
+ end_index = (end_row if end_row is not None else start_row) + 1 # exclusive
830
+
831
+ if auto_resize:
832
+ return await _auto_resize_dimension(
833
+ sheets_service, spreadsheet_id, sheet_id, "ROWS", start_row, end_index
834
+ )
835
+
836
+ return await _set_dimension_pixel_size(
837
+ sheets_service, spreadsheet_id, sheet_id, "ROWS", start_row, end_index, pixel_size
838
+ )
839
+
840
+ @tool(annotations=ToolAnnotations(title="Resize Columns", destructiveHint=True))
841
+ async def resize_columns(
842
+ spreadsheet_id: str,
843
+ sheet: str,
844
+ start_column: int,
845
+ end_column: int | None = None,
846
+ pixel_size: int | None = None,
847
+ auto_resize: bool = False,
848
+ ctx: Context = None,
849
+ ) -> dict[str, Any]:
850
+ """
851
+ Set an explicit pixel width for columns, or auto-fit them to content.
852
+
853
+ Args:
854
+ spreadsheet_id: The ID of the spreadsheet (found in the URL)
855
+ sheet: The name of the sheet
856
+ start_column: 0-based index of the first column to resize (0 = column A)
857
+ end_column: 0-based index of the last column to resize (inclusive).
858
+ If omitted, resizes only start_column.
859
+ pixel_size: Explicit column width in pixels. Mutually exclusive with auto_resize.
860
+ auto_resize: If True, auto-fit column width to content instead of setting
861
+ an explicit pixel_size.
862
+
863
+ Returns:
864
+ Result of the batchUpdate operation
865
+ """
866
+ lc = ctx.request_context.lifespan_context
867
+ sheets_service = lc.sheets_service
868
+
869
+ if pixel_size is None and not auto_resize:
870
+ return {"error": "Specify pixel_size or set auto_resize=True"}
871
+ if pixel_size is not None and auto_resize:
872
+ return {"error": "Specify only one of pixel_size or auto_resize"}
873
+
874
+ sheet_id = await _get_sheet_id(
875
+ sheets_service, spreadsheet_id, sheet, lc.cache, lc.drive_service
876
+ )
877
+ if sheet_id is None:
878
+ return {"error": f"Sheet '{sheet}' not found"}
879
+
880
+ end_index = (end_column if end_column is not None else start_column) + 1 # exclusive
881
+
882
+ if auto_resize:
883
+ return await _auto_resize_dimension(
884
+ sheets_service, spreadsheet_id, sheet_id, "COLUMNS", start_column, end_index
885
+ )
886
+
887
+ return await _set_dimension_pixel_size(
888
+ sheets_service, spreadsheet_id, sheet_id, "COLUMNS", start_column, end_index, pixel_size
889
+ )
890
+
722
891
  @tool(annotations=ToolAnnotations(title="Format Cells", destructiveHint=True))
723
892
  async def format_cells(
724
893
  spreadsheet_id: str,
@@ -694,6 +694,165 @@ class TestUnhideColumns:
694
694
  assert "error" in result
695
695
 
696
696
 
697
+ class TestResizeRows:
698
+ def _sheets_service(self, sheet_id=0):
699
+ mock = MagicMock()
700
+ mock.spreadsheets.return_value.get.return_value.execute.return_value = {
701
+ "sheets": [{"properties": {"title": "Sheet1", "sheetId": sheet_id}}]
702
+ }
703
+ mock.spreadsheets.return_value.batchUpdate.return_value.execute.return_value = {}
704
+ return mock
705
+
706
+ def _request(self, svc):
707
+ body = svc.spreadsheets.return_value.batchUpdate.call_args.kwargs["body"]
708
+ return body["requests"][0]
709
+
710
+ async def test_pixel_size_sets_updateDimensionProperties(self):
711
+ svc = self._sheets_service()
712
+ ctx = _make_ctx(sheets_service=svc, cache=None)
713
+ await _structure_tools["resize_rows"](
714
+ spreadsheet_id="ss1", sheet="Sheet1", start_row=3, pixel_size=50, ctx=ctx
715
+ )
716
+ req = self._request(svc)["updateDimensionProperties"]
717
+ assert req["range"]["startIndex"] == 3
718
+ assert req["range"]["endIndex"] == 4
719
+ assert req["range"]["dimension"] == "ROWS"
720
+ assert req["properties"] == {"pixelSize": 50}
721
+ assert req["fields"] == "pixelSize"
722
+
723
+ async def test_range_of_rows_end_index_is_inclusive(self):
724
+ svc = self._sheets_service()
725
+ ctx = _make_ctx(sheets_service=svc, cache=None)
726
+ await _structure_tools["resize_rows"](
727
+ spreadsheet_id="ss1", sheet="Sheet1", start_row=2, end_row=5, pixel_size=30, ctx=ctx
728
+ )
729
+ req = self._request(svc)["updateDimensionProperties"]
730
+ assert req["range"]["startIndex"] == 2
731
+ assert req["range"]["endIndex"] == 6 # end_row=5 inclusive → exclusive index 6
732
+
733
+ async def test_auto_resize_sets_autoResizeDimensions(self):
734
+ svc = self._sheets_service()
735
+ ctx = _make_ctx(sheets_service=svc, cache=None)
736
+ await _structure_tools["resize_rows"](
737
+ spreadsheet_id="ss1", sheet="Sheet1", start_row=3, auto_resize=True, ctx=ctx
738
+ )
739
+ req = self._request(svc)["autoResizeDimensions"]
740
+ assert req["dimensions"]["startIndex"] == 3
741
+ assert req["dimensions"]["endIndex"] == 4
742
+ assert req["dimensions"]["dimension"] == "ROWS"
743
+
744
+ async def test_returns_error_when_neither_option_given(self):
745
+ svc = self._sheets_service()
746
+ ctx = _make_ctx(sheets_service=svc, cache=None)
747
+ result = await _structure_tools["resize_rows"](
748
+ spreadsheet_id="ss1", sheet="Sheet1", start_row=0, ctx=ctx
749
+ )
750
+ assert "error" in result
751
+
752
+ async def test_returns_error_when_both_options_given(self):
753
+ svc = self._sheets_service()
754
+ ctx = _make_ctx(sheets_service=svc, cache=None)
755
+ result = await _structure_tools["resize_rows"](
756
+ spreadsheet_id="ss1",
757
+ sheet="Sheet1",
758
+ start_row=0,
759
+ pixel_size=50,
760
+ auto_resize=True,
761
+ ctx=ctx,
762
+ )
763
+ assert "error" in result
764
+
765
+ async def test_returns_error_when_sheet_not_found(self):
766
+ svc = self._sheets_service()
767
+ ctx = _make_ctx(sheets_service=svc, cache=None)
768
+ result = await _structure_tools["resize_rows"](
769
+ spreadsheet_id="ss1", sheet="Missing", start_row=0, pixel_size=50, ctx=ctx
770
+ )
771
+ assert "error" in result
772
+
773
+
774
+ class TestResizeColumns:
775
+ def _sheets_service(self, sheet_id=0):
776
+ mock = MagicMock()
777
+ mock.spreadsheets.return_value.get.return_value.execute.return_value = {
778
+ "sheets": [{"properties": {"title": "Sheet1", "sheetId": sheet_id}}]
779
+ }
780
+ mock.spreadsheets.return_value.batchUpdate.return_value.execute.return_value = {}
781
+ return mock
782
+
783
+ def _request(self, svc):
784
+ body = svc.spreadsheets.return_value.batchUpdate.call_args.kwargs["body"]
785
+ return body["requests"][0]
786
+
787
+ async def test_pixel_size_sets_updateDimensionProperties(self):
788
+ svc = self._sheets_service()
789
+ ctx = _make_ctx(sheets_service=svc, cache=None)
790
+ await _structure_tools["resize_columns"](
791
+ spreadsheet_id="ss1", sheet="Sheet1", start_column=0, pixel_size=120, ctx=ctx
792
+ )
793
+ req = self._request(svc)["updateDimensionProperties"]
794
+ assert req["range"]["startIndex"] == 0
795
+ assert req["range"]["endIndex"] == 1
796
+ assert req["range"]["dimension"] == "COLUMNS"
797
+ assert req["properties"] == {"pixelSize": 120}
798
+ assert req["fields"] == "pixelSize"
799
+
800
+ async def test_range_of_columns_end_index_is_inclusive(self):
801
+ svc = self._sheets_service()
802
+ ctx = _make_ctx(sheets_service=svc, cache=None)
803
+ await _structure_tools["resize_columns"](
804
+ spreadsheet_id="ss1",
805
+ sheet="Sheet1",
806
+ start_column=1,
807
+ end_column=3,
808
+ pixel_size=80,
809
+ ctx=ctx,
810
+ )
811
+ req = self._request(svc)["updateDimensionProperties"]
812
+ assert req["range"]["startIndex"] == 1
813
+ assert req["range"]["endIndex"] == 4 # end_column=3 inclusive → exclusive index 4
814
+
815
+ async def test_auto_resize_sets_autoResizeDimensions(self):
816
+ svc = self._sheets_service()
817
+ ctx = _make_ctx(sheets_service=svc, cache=None)
818
+ await _structure_tools["resize_columns"](
819
+ spreadsheet_id="ss1", sheet="Sheet1", start_column=0, auto_resize=True, ctx=ctx
820
+ )
821
+ req = self._request(svc)["autoResizeDimensions"]
822
+ assert req["dimensions"]["startIndex"] == 0
823
+ assert req["dimensions"]["endIndex"] == 1
824
+ assert req["dimensions"]["dimension"] == "COLUMNS"
825
+
826
+ async def test_returns_error_when_neither_option_given(self):
827
+ svc = self._sheets_service()
828
+ ctx = _make_ctx(sheets_service=svc, cache=None)
829
+ result = await _structure_tools["resize_columns"](
830
+ spreadsheet_id="ss1", sheet="Sheet1", start_column=0, ctx=ctx
831
+ )
832
+ assert "error" in result
833
+
834
+ async def test_returns_error_when_both_options_given(self):
835
+ svc = self._sheets_service()
836
+ ctx = _make_ctx(sheets_service=svc, cache=None)
837
+ result = await _structure_tools["resize_columns"](
838
+ spreadsheet_id="ss1",
839
+ sheet="Sheet1",
840
+ start_column=0,
841
+ pixel_size=80,
842
+ auto_resize=True,
843
+ ctx=ctx,
844
+ )
845
+ assert "error" in result
846
+
847
+ async def test_returns_error_when_sheet_not_found(self):
848
+ svc = self._sheets_service()
849
+ ctx = _make_ctx(sheets_service=svc, cache=None)
850
+ result = await _structure_tools["resize_columns"](
851
+ spreadsheet_id="ss1", sheet="Missing", start_column=0, pixel_size=80, ctx=ctx
852
+ )
853
+ assert "error" in result
854
+
855
+
697
856
  class TestFormatCells:
698
857
  def _sheets_service(self, sheet_id=0):
699
858
  mock = MagicMock()