mcp-gee-sweet 0.7.0.dev80__tar.gz → 0.7.0.dev84__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 (143) hide show
  1. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.claude/commands/merge-pr.md +2 -1
  2. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.claude/commands/orchestrator.md +1 -1
  3. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.claude/commands/prep-for-pr.md +1 -1
  4. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.claude/commands/verify-pr.md +1 -1
  5. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/CLAUDE.md +1 -1
  6. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/PKG-INFO +1 -1
  7. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/configuration.md +5 -3
  8. mcp_gee_sweet-0.7.0.dev84/docs/decisions/decision-cache-invalidation.md +51 -0
  9. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/index.md +1 -0
  10. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/design/docs-ast-pipeline.md +14 -0
  11. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/docs.md +19 -0
  12. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/infra.md +60 -0
  13. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/roadmap.md +8 -7
  14. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/tools.md +3 -1
  15. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/.env.template +5 -1
  16. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/cache.py +211 -145
  17. mcp_gee_sweet-0.7.0.dev84/src/mcp_gee_sweet/tools/cache.py +109 -0
  18. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/content.py +22 -2
  19. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/emitter.py +57 -23
  20. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/sheets/data.py +38 -7
  21. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/sheets/helpers.py +2 -1
  22. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/sheets/structure.py +12 -12
  23. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_cache.py +241 -0
  24. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_docs_content.py +51 -0
  25. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_docs_tables.py +120 -0
  26. mcp_gee_sweet-0.7.0.dev80/src/mcp_gee_sweet/tools/cache.py +0 -54
  27. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.claude/commands/cleanup-worktrees.md +0 -0
  28. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.claude/commands/start-worktree.md +0 -0
  29. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.dockerignore +0 -0
  30. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/dependabot.yml +0 -0
  34. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/workflows/ci.yml +0 -0
  35. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/workflows/docs.yml +0 -0
  36. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/workflows/publish-dev.yml +0 -0
  37. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.github/workflows/release.yml +0 -0
  38. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.gitignore +0 -0
  39. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.pre-commit-config.yaml +0 -0
  40. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/.python-version +0 -0
  41. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/CHANGELOG.md +0 -0
  42. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/CODE_OF_CONDUCT.md +0 -0
  43. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/CONTRIBUTING.md +0 -0
  44. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/Dockerfile +0 -0
  45. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/LICENSE +0 -0
  46. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/Makefile +0 -0
  47. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/README.md +0 -0
  48. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/SECURITY.md +0 -0
  49. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docker-compose.yml +0 -0
  50. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/auth.md +0 -0
  51. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/client-setup.md +0 -0
  52. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-chart-theming.md +0 -0
  53. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-composite-tools.md +0 -0
  54. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-docs-formatting.md +0 -0
  55. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-fork.md +0 -0
  56. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  57. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-publishing.md +0 -0
  58. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  59. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/decisions/decision-testing.md +0 -0
  60. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/design/effectiveformat-spike.md +0 -0
  61. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/design/index.md +0 -0
  62. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/design/markdown-support.md +0 -0
  63. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/design/phase3-theme-system.md +0 -0
  64. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/design.md +0 -0
  65. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/index.md +0 -0
  66. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/known-limitations.md +0 -0
  67. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/notes-read.md +0 -0
  68. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/README.md +0 -0
  69. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  70. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  71. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  72. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  73. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/fixtures.template.md +0 -0
  74. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/operations.yaml +0 -0
  75. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/playwright_oauth.md +0 -0
  76. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/.gitkeep +0 -0
  77. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-02.md +0 -0
  78. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-14.md +0 -0
  79. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-17.md +0 -0
  80. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-18.md +0 -0
  81. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  82. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  83. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-06-28.md +0 -0
  84. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-07-04-docs.md +0 -0
  85. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  86. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-07-04-infra.md +0 -0
  87. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  88. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/results/2026-07-04.md +0 -0
  89. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/retro-v0.8.0.md +0 -0
  90. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/run.md +0 -0
  91. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/runs/README.md +0 -0
  92. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/runs/v0.8.0.md +0 -0
  93. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/runs/v0.8.1.md +0 -0
  94. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/setup.md +0 -0
  95. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/calendar.md +0 -0
  96. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/drive.md +0 -0
  97. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/sheets_charts.md +0 -0
  98. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/sheets_mgmt.md +0 -0
  99. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/sheets_read.md +0 -0
  100. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa/tests/sheets_write.md +0 -0
  101. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/docs/qa-checklist.md +0 -0
  102. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/mkdocs.yml +0 -0
  103. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/pyproject.toml +0 -0
  104. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/pyrightconfig.json +0 -0
  105. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/scripts/gen_tool_docs.py +0 -0
  106. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/scripts/oauth_setup.py +0 -0
  107. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/__init__.py +0 -0
  108. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/auth.py +0 -0
  109. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/server.py +0 -0
  110. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  111. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  112. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  113. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  114. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  115. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  116. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  117. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  118. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  119. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  120. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  121. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  122. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  123. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  124. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  125. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/__init__.py +0 -0
  126. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/drive/__init__.py +0 -0
  127. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/drive/test_activity.py +0 -0
  128. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/drive/test_files.py +0 -0
  129. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/drive/test_sharing.py +0 -0
  130. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/drive/test_transfer.py +0 -0
  131. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/sheets/__init__.py +0 -0
  132. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/sheets/test_data.py +0 -0
  133. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/sheets/test_helpers.py +0 -0
  134. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/sheets/test_structure.py +0 -0
  135. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_auth.py +0 -0
  136. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_calendar.py +0 -0
  137. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_docs_core.py +0 -0
  138. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_docs_layout.py +0 -0
  139. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_docs_style.py +0 -0
  140. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_gen_tool_docs.py +0 -0
  141. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_response_limits.py +0 -0
  142. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/tests/test_server.py +0 -0
  143. {mcp_gee_sweet-0.7.0.dev80 → mcp_gee_sweet-0.7.0.dev84}/uv.lock +0 -0
@@ -10,4 +10,5 @@ Merge the current branch's PR and clean up its worktree. Uses `--admin` only to
10
10
  5. Clean up the local worktree:
11
11
  - If this session created the current worktree (i.e. it got here via `/start-worktree` or `/next-issue` earlier in this same session), use `ExitWorktree` with `action: "remove"`.
12
12
  - Otherwise (worktree predates this session), fall back to manual cleanup from the main checkout: `git worktree remove <path>`, then `git branch -d <branch>`, then `git worktree prune`.
13
- 6. Report a summary: the merge commit, the now-merged PR URL, and confirmation the worktree was removed.
13
+ 6. Update the roadmap: from the main checkout on `develop`, `git pull` then check `docs/roadmap.md` for a checkbox line referencing the issue(s) this PR closes (search by issue number). If found, check it off (`- [ ]` → `- [x]`) and append `(PR <number>)` in the existing style (e.g. `(PR #280)`) — see recent `- [x]` entries for the exact convention. If the roadmap has no line item for this issue (not every ticket is roadmap-tracked — chores, follow-up defects, etc.), skip this step silently. Commit directly to `develop` (`docs(roadmap): check off #<issue> (PR <number>)`) and push — `develop`'s branch protection has `enforce_admins: false`, so a direct push by the repo owner is allowed for this kind of small, reversible, docs-only change; don't open a separate PR for it.
14
+ 7. Report a summary: the merge commit, the now-merged PR URL, confirmation the worktree was removed, and whether the roadmap was updated.
@@ -11,7 +11,7 @@ Why this role split exists: any locally-configured MCP server that runs this pro
11
11
  - `gh pr list --state open` — PRs awaiting live-test/review/merge (run from inside the repo so `gh` infers it from the git remote — don't hardcode a repo slug, forks won't match)
12
12
  - `gh issue list --label ready-for-development --state open` — queued ticket(s)
13
13
  4. State the orchestrator's responsibilities plainly:
14
- - Ticket triage and labeling (`ready-for-development`, version labels)
14
+ - Ticket triage and labeling (`ready-for-development`, version labels). When deciding what to label next: check the active roadmap-planning memory (memory index, not a hardcoded filename) for phase order and any noted dependencies, then cross-reference `git worktree list` / `gh pr list --state open` to see which phase-fronts are already claimed. Label only the single next unclaimed ticket per active phase — not a whole phase or the backlog. Before labeling a ticket to run *in parallel* with other in-flight work, check whether the roadmap memory flags it as cross-cutting or conflict-risk (e.g. a rewrite touching most/all tool files); if so, hold it for a solo slot once only one session is active instead of parallelizing it.
15
15
  - Roadmap grooming (`docs/roadmap.md` + whichever roadmap-planning memory is currently active — check the memory index rather than assuming a specific version-numbered file)
16
16
  - Live/manual tool testing — only possible here
17
17
  - Reviewing worker PRs: `verify-pr` (code review + scoped live QA against real Google APIs) before `merge-pr`
@@ -7,7 +7,7 @@ Review the current branch against this checklist and report the status of each i
7
7
  - [ ] **Unit tests written** — are there new tests in `tests/` covering the changed code?
8
8
  - [ ] **Unit tests passing** — has `uv run python -m pytest tests/` been run and passed?
9
9
  - [ ] **Regression coverage** — were tests that touch the modified files (not just new tests) also run?
10
- - [ ] **QA test cases written** — are there AI-driven test cases in `docs/qa/tests/` for the new/changed tools?
10
+ - [ ] **QA test cases written** — are there AI-driven test cases in `docs/qa/tests/` covering the changed behavior? This applies beyond new tools: a change to shared internal code under `src/mcp_gee_sweet/tools/` (e.g. `docs/emitter.py`, `sheets/helpers.py`) that alters what a live tool call produces needs a test case too — a unit test alone confirms the function's internal contract, not the real API output. If this is a bug fix, the test case should reproduce the regression scenario itself, not just spot-check the tool's happy path.
11
11
 
12
12
  **Live QA execution is out of scope for this checklist.** If this session is a worker in a `.claude/worktrees/*` checkout, running live QA tools here would exercise the main checkout's code, not this branch's changes — the result would look real but prove nothing. Do not run live tests and do not write `**Result**` entries from a worktree. That happens in the orchestrator's `/verify-pr` pass after the PR is open, in the main checkout, where the branch's actual code is reachable by the live MCP tools. Leave the `**Result**` line off new/changed test cases entirely — don't stub it as "pending," just omit it so `/verify-pr` adds the first real one.
13
13
 
@@ -4,7 +4,7 @@ CI (the "Lint and test" check `/merge-pr` already gates on) covers unit tests. I
4
4
 
5
5
  1. **Preconditions.** Confirm this session is in the main checkout, not a worktree (same check as `/orchestrator` step 1). If the main checkout has uncommitted changes, stop and ask the user how to proceed — don't stash or discard anything.
6
6
 
7
- 2. **Identify the PR.** Take a PR number as an argument if given, otherwise use `gh pr list --state open` and ask the user which one if there's more than one. Get its details: `gh pr view <number> --json number,title,url,headRefName,baseRefName,statusCheckRollup`.
7
+ 2. **Identify the PR.** Take a PR number as an argument if given, otherwise use `gh pr list --state open` and ask the user which one if there's more than one. Get its details: `gh pr view <number> --json number,title,url,headRefName,baseRefName,statusCheckRollup,mergeable,mergeStateStatus`. If `mergeable` is `CONFLICTING`, stop here — report it and say the branch needs a rebase onto `<baseRefName>` before verification is worth doing. Don't spend code-review or live-QA effort on a branch that can't merge as-is; that cost is only worth paying once it's rebased.
8
8
 
9
9
  3. **Check out the branch.** `gh pr checkout <number>`. This fetches and switches the main checkout to the PR's branch directly (no worktree needed — the orchestrator owns the main checkout).
10
10
 
@@ -46,7 +46,7 @@ Logic is split across `src/mcp_gee_sweet/`: `server.py` (MCP setup, tool decorat
46
46
  - `docs/ast.py` — dataclass schema
47
47
  - `docs/html_parser.py` — HTML→AST
48
48
  - `docs/emitter.py` — AST→Docs API requests + multi-phase table fill including nested table and cell styling support
49
- - `tools/cache.py` — `refresh_cache`
49
+ - `tools/cache.py` — `refresh_cache`, `set_cache_ttl`/`get_cache_ttl` (runtime TTL change, no restart needed)
50
50
  - `tools/calendar.py` — Calendar API tools
51
51
  - `tools/response_limits.py` — shared response-size safety net (`enforce_response_size_cap`, `write_capped_result_to_disk`); imported cross-package the same way `tools/drive/__init__.py`'s `_SA_QUOTA_ERROR` is. Cap configured via `MAX_TOOL_RESPONSE_CHARS` (default 40000); see `docs/decisions/decision-response-size-cap-generalization.md`
52
52
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-gee-sweet
3
- Version: 0.7.0.dev80
3
+ Version: 0.7.0.dev84
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
@@ -13,7 +13,8 @@
13
13
  | `CREDENTIALS_CONFIG` | Base64-encoded credentials JSON (for containers) | — |
14
14
  | `ENABLED_TOOLS` | Comma-separated list of tool names to register | all tools |
15
15
  | `CACHE_DB_PATH` | Path to the SQLite cache database | `/tmp/mcp_gee_sweet.db` |
16
- | `CACHE_TTL` | Cache time-to-live in seconds | `1800` (30 min) |
16
+ | `CACHE_TTL` | Cache time-to-live in seconds. Also adjustable at runtime via the `set_cache_ttl` tool, no restart needed | `1800` (30 min) |
17
+ | `CACHE_VALIDATE_MODIFIED_TIME` | Validate sheet/doc cache hits against Drive's `modifiedTime` before serving them, catching edits from other sessions without waiting out the TTL. Costs one extra Drive API call per cache lookup | `true` |
17
18
  | `MAX_TOOL_RESPONSE_CHARS` | Safety cap on response size, in characters, for tools that can return large inline payloads (`get_sheet_data`, `get_multiple_sheet_data`, `find_in_spreadsheet`, `get_doc_content`, `export_file`, `list_file_activity`). Raise to match a raised `MAX_MCP_OUTPUT_TOKENS` in your MCP client (roughly 2 characters per token observed for densely-formatted JSON) | `40000` |
18
19
  | `HOST` (or `FASTMCP_HOST`) | Bind address for SSE transport. `FASTMCP_HOST` is a fallback if `HOST` isn't set | `0.0.0.0` |
19
20
  | `PORT` (or `FASTMCP_PORT`) | Port for SSE transport. `FASTMCP_PORT` is a fallback if `PORT` isn't set | `8000` |
@@ -69,7 +70,7 @@ Access logs are emitted at `INFO` level — they appear when `DEBUG_LEVEL` is `D
69
70
 
70
71
  <!-- Auto-generated by `scripts/gen_tool_docs.py` — do not edit by hand, your changes will be overwritten on the next commit. -->
71
72
 
72
- By default all 90 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 92 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
74
 
74
75
  ```bash
75
76
  # Environment variable
@@ -109,8 +110,9 @@ See [Tools](tools.md) for the full list of tool names.
109
110
 
110
111
  The server caches responses in a local SQLite database to reduce API calls and latency. Five namespaces are cached: sheet structure, sheet data, Drive folder listings, doc content, and calendar metadata.
111
112
 
112
- - TTL-based expiry: controlled by `CACHE_TTL` (default 30 minutes)
113
+ - TTL-based expiry: controlled by `CACHE_TTL` (default 30 minutes); change it at runtime with the `set_cache_ttl` tool instead of restarting the server
113
114
  - Dirty invalidation: write operations mark the relevant cache entries stale immediately
115
+ - Change-based invalidation: for sheet structure/data and doc content (the namespaces keyed by a single Drive file), a cache hit is checked against that file's live `modifiedTime` before being served — an edit from another session or tab invalidates the entry immediately instead of waiting out the TTL. Controlled by `CACHE_VALIDATE_MODIFIED_TIME` (default `true`); disable it if the extra Drive API call per lookup isn't worth the freshness guarantee for your usage pattern. Not applied to Drive folder listings (a folder's own `modifiedTime` doesn't change when its children do) or calendar metadata (no comparable field via the Calendar API)
114
116
  - Scoped flush: `refresh_cache` accepts a `spreadsheet_id`, `doc_id`, `folder_id`, or `calendar_id` to invalidate only that resource; omit all params to flush everything
115
117
 
116
118
  The DB path defaults to `/tmp/mcp_gee_sweet.db`. Set `CACHE_DB_PATH` to a persistent location if you want the cache to survive container restarts.
@@ -0,0 +1,51 @@
1
+ # Decision: Runtime Cache TTL and modifiedTime-Based Invalidation (issue #99)
2
+
3
+ **Date:** 2026-07-07
4
+ **Snapshot commit:** branch `feat/issue-99` — see `src/mcp_gee_sweet/cache.py`
5
+
6
+ ## Background
7
+
8
+ The cache's only staleness defenses were a fixed `CACHE_TTL` (set at process start, unchangeable without a restart) and `mark_dirty` calls from this server's own write tools. Neither catches an edit that happens outside this MCP session — another Claude session, or a person editing the spreadsheet/doc directly — until the TTL window closes. Issue #99 asked for two independent improvements: a way to change the TTL without restarting, and a way to detect out-of-band edits sooner.
9
+
10
+ ## Decisions
11
+
12
+ ### 1. `set_cache_ttl` tool, not a per-call TTL parameter
13
+
14
+ The issue suggested either a `set_cache_ttl` tool or a per-resource TTL parameter added to every read tool. Chose the tool: it's one new surface instead of a parameter threaded through a dozen existing signatures, and TTL is a deployment-wide tuning knob in practice (this codebase already treats `CACHE_TTL` as global, not per-call). The tool updates `_ttl` on all five in-memory cache instances immediately; it doesn't touch the `CACHE_TTL` env var, so it only affects the running process, not the startup default.
15
+
16
+ ### 2. `modified_time` stored inside the cached JSON value, not a new DB column
17
+
18
+ Considered adding a `source_modified_time` column to the `cache` table. Rejected — it would need a migration path for every already-deployed SQLite file (`ALTER TABLE ... ADD COLUMN` guarded against pre-existing installs), for a value that's only ever read back by the same process that wrote it. Storing `modified_time` as a key inside the existing JSON `value` blob avoids the migration entirely and, for `DocContentCache`, turned out to already be present — `get_doc_content` was already fetching and storing `modifiedTime` as part of the cached doc dict, so that cache needed no `store()` signature change at all, only a comparison added to `_get_valid`.
19
+
20
+ ### 3. Applied to sheet structure/data and doc content only — not Drive folder listings or calendar metadata
21
+
22
+ The issue's own framing ("a spreadsheet or doc is being edited by others") pointed at file-keyed caches. Checked whether the other two namespaces could reuse the same mechanism and concluded no for structural reasons, not just scope-trimming:
23
+
24
+ - **`DriveFolderCache`**: a folder's own `modifiedTime` does not change when a child file is added, removed, or renamed — Drive doesn't propagate child mutations to the parent's metadata. The signal this feature depends on doesn't exist for folder listings.
25
+ - **`CalendarCache`**: calendars aren't Drive files and the Calendar API doesn't expose a directly comparable field via the endpoints already in use (`calendarList().list()` / `calendars().get()`). Out of scope without a separate investigation into what Calendar API field, if any, would serve the same purpose.
26
+
27
+ Both classes still got `set_ttl` (decision 1 applies uniformly); only the modifiedTime check is scoped down.
28
+
29
+ ### 4. Validation is opt-out (`CACHE_VALIDATE_MODIFIED_TIME`, default `true`), not opt-in
30
+
31
+ The issue's own "Notes" section flagged the cost explicitly: one extra Drive `files.get(fields=modifiedTime)` call per cache lookup, and asked whether that overhead is worth it relative to the cost of a stale read. Without a production benchmark available, defaulted to correctness (matching the problem the issue opened with — staleness that fails silently) and exposed the env var as the escape hatch for deployments where the extra round-trip matters more than immediate freshness.
32
+
33
+ ### 5. `drive_service` threaded through every `fetch_sheets()` call site, including `_get_sheet_id`
34
+
35
+ **Reversed from the original PR.** The first version deliberately left `_get_sheet_id()` (`tools/sheets/helpers.py`) — the internal sheet-name→ID resolver used by ~11 write-path tools in `structure.py` (`add_rows`, `format_cells`, etc.) — calling `fetch_sheets()` without `drive_service`, reasoning that write-path correctness was a separate concern from read-path freshness (decision 3's cost/scope framing).
36
+
37
+ Code review caught that this was a correctness bug, not a scoping choice: `SheetStructureCache` rows are keyed only by `spreadsheet_id` and shared across every call path. `_get_sheet_id`'s 3-arg `fetch_sheets()` call computed `current_mtime=None` (no `drive_service`), and on a cache miss `store()` only writes the `modified_time` key when it's not `None` — so the row got re-stored *without* a `modified_time` key at all. The next reader's `_check_modified_time` sees `cached_mtime is None` and skips the comparison entirely (there's nothing to compare against), silently disabling staleness detection for that spreadsheet for every subsequent caller — including the correctly-updated `list_sheets`/`find_in_spreadsheet`/`get_multiple_spreadsheet_summary` paths. Since almost every mutating sheet tool routes through `_get_sheet_id`, this defeated a large fraction of the feature in practice. Fixed by threading `drive_service` through `_get_sheet_id` and all 11 call sites; regression-tested in `tests/test_cache.py::TestGetSheetIdModifiedTimePropagation`.
38
+
39
+ ### 6. `get_cache_ttl` tool added alongside `set_cache_ttl`
40
+
41
+ `set_cache_ttl` mutates 5 process-wide singleton cache instances shared by every concurrent SSE client session, with no prior way to inspect the current value before overwriting it — one session calling `set_cache_ttl(0)` silently changed behavior for everyone else with no way to know what to restore. Added a read-only `get_cache_ttl` tool (backed by a new `_BaseCache.get_ttl()`) and a docstring/log line on `set_cache_ttl` making the process-wide, multi-session blast radius explicit. The underlying single-process-wide-cache design itself is unchanged (decision 1 still applies) — this only closes the "can't see or restore the previous value" gap.
42
+
43
+ ### 7. Shared `_BaseCache` for the TTL/modifiedTime-check plumbing
44
+
45
+ The modifiedTime-comparison-and-mark-dirty block was duplicated near-verbatim across three cache classes, and `set_ttl`/`close`/`mark_all_dirty` were duplicated across all five, with no single source of truth — exactly the shape of bug that produced decision 5's regression (one call site missed a signature update, and nothing forced the other four to be checked). Extracted a `_BaseCache` with `_check_modified_time`/`_check_ttl` helpers and the shared connection/TTL/dirty-marking methods; each cache class now only owns its own key shape and payload shape. `_get_valid` methods also now parse the cached JSON value once and return it directly instead of returning the raw `sqlite3.Row` for callers to re-parse.
46
+
47
+ ## When to Re-evaluate
48
+
49
+ - If Calendar API staleness turns out to matter in practice, investigate what field (if any) the Calendar API exposes that's comparable to Drive's `modifiedTime` before assuming this pattern extends there directly.
50
+ - No live benchmark of the extra `files.get` call's latency impact has been run (decision 4) — if `CACHE_VALIDATE_MODIFIED_TIME` overhead is reported as a real problem, that's the first thing to measure. `get_multiple_spreadsheet_summary` now makes one such call per sheet (not per spreadsheet) to keep each sheet's cached `modified_time` tag accurate against its own, separately-timed fetch — if that tool's Drive-call volume becomes a problem for spreadsheets with many sheets, this is the place to revisit first.
51
+ - `get_cache_ttl`/`set_cache_ttl` still have no per-session scoping (decision 6) — if that becomes a real operational problem in multi-session SSE deployments, the fix is per-session cache instances, not another env var.
@@ -11,3 +11,4 @@ Point-in-time records of specific decisions — context, alternatives, and reaso
11
11
  | [Docs Formatting Architecture](decision-docs-formatting.md) | 2026-06-17 | decided | Two-layer approach (direct API tools + HTML abstraction via AST); phase plan for #41, #65–#70, #79–#82 |
12
12
  | [Chart Theming and Phase 3 Integration](decision-chart-theming.md) | 2026-06-17 | decided | No parsing layer for charts; no Vega-Lite adoption; unified brand config (docs + charts sections) for Phase 3 theme system |
13
13
  | [Grid Data Size Cap](decision-grid-data-size-cap.md) | 2026-07-03 | decided | Post-fetch response-size check (not pre-fetch cell-count estimate) for `get_sheet_data(include_grid_data=True)`; configurable cap + `local_path` bypass; empirically falsified two earlier approaches |
14
+ | [Cache Invalidation](decision-cache-invalidation.md) | 2026-07-07 | decided | `set_cache_ttl` tool for runtime TTL; `modifiedTime`-based validation for sheet/doc caches only (not folders/calendar); opt-out via `CACHE_VALIDATE_MODIFIED_TIME` |
@@ -338,3 +338,17 @@ Cells with no `Table` in `children` are still filled in one bulk batch by `_buil
338
338
  - Text before, between, and after any number of nested tables in one cell renders in true source order, at any nesting depth.
339
339
  - The "one level of nesting only" and "no colspan/rowspan/col_widths inside nested tables" limitations from the original #92 design are otherwise unchanged — this rewrite only touches content ordering, not those structural features.
340
340
  - Tested with `tests/test_docs_tables.py`'s `TestFillNestedCellContent`, which drives the real algorithm against a small in-memory Docs-API simulator (`FakeDoc`/`Para`/`TableNode` in that file) that actually applies `insertText`/`insertTable` mutations and re-derives indices — chosen over hand-computed index fixtures because this much re-fetch/recursion logic is easy to get subtly wrong in a way a fixture can't catch (and did, twice, during development).
341
+
342
+ ---
343
+
344
+ ## Nested table colspan/rowspan (#109)
345
+
346
+ **Date:** 2026-07-08
347
+
348
+ Nested tables produced a correctly-sized shell but never merged cells with `colspan`/`rowspan` > 1 — `_fill_table_fully` (the per-nested-table fill helper introduced in the rewrite above) only ran the plain-cell bulk fill, with no equivalent of the outer-table merge phase (`fill_tables` phase 2).
349
+
350
+ Fix: `_fill_table_fully` now checks the nested `ast_table` for any cell with `colspan > 1 or rowspan > 1` and, if found, builds and executes `mergeTableCells` requests via the existing `_build_merge_requests` (unchanged — it already operated generically on any `doc_table`/`ast_table` pair, nested or not) before re-fetching and running the bulk fill. This exactly mirrors `fill_tables`'s own phase 1→2→3 order, just scoped to one nested table.
351
+
352
+ `col_widths` on nested tables remains unsupported — out of scope for #109, not addressed here.
353
+
354
+ Tested in `tests/test_docs_tables.py`'s `TestNestedTableMerges`, extending the same `FakeDoc` simulator with a no-op `mergeTableCells` handler (the real Docs API doesn't shift indices or delete content on merge — covered cells stay physical, just skipped by the existing phantom-cell mapping — so the simulator has nothing to mutate; the tests instead assert the merge request's shape and its position in batch order relative to the fill).
@@ -1038,6 +1038,25 @@ These test the HTML→AST→Docs API pipeline introduced in Phase 2 (#87). All u
1038
1038
 
1039
1039
  ---
1040
1040
 
1041
+ ### TC-DOC90: `colspan`/`rowspan` inside a nested table now merges correctly (issue #109) ⚠️ destructive
1042
+
1043
+ **Background:** Nested tables produced a correctly-sized shell but silently ignored `colspan`/`rowspan` on their own cells — no `mergeTableCells` request was ever emitted for them, unlike outer-table cells (TC-DOC34/36/37). Fixed by having `_fill_table_fully` run the same merge phase for a nested table's own cells that `fill_tables` already runs for the outer table.
1044
+
1045
+ **Prompt**
1046
+ > "Write this HTML to doc {DOC_ID}: `<table><tr><td><table><tr><td colspan=\"2\">Header</td></tr><tr><td>A</td><td>B</td></tr></table></td></tr></table>`"
1047
+
1048
+ **Checks**
1049
+ - Call succeeds with no API error
1050
+ - `get_doc_structure` shows the outer table (1 row × 1 col) with cell [0,0] text empty (holds the nested table, not text)
1051
+ - 🔍 Visual check: nested table renders with "Header" spanning both columns of the top row, and "A"/"B" as two separate cells in the second row — not four ungrouped cells
1052
+
1053
+ **Cleanup:** write fixture content back
1054
+
1055
+ **Result (2026-07-09) ✅ PASS**
1056
+ `write_doc_content` succeeded. `get_doc_structure` showed the outer table (1 row × 1 col) with cell [0,0] text empty. Playwright screenshot confirmed the nested table rendered with "Header" spanning both columns of the top row and "A"/"B" as two separate cells below — the merge applied correctly. (Unrelated observation: the fixture doc had leftover header/footer text visible in the render and in `get_doc_content`'s plain-text export but not in `get_doc_structure` — headers/footers aren't part of the body map that tool returns; pre-existing fixture-doc state from an earlier header/footer test, untouched by `write_doc_content`, not a regression from this PR.) Fixture content restored per cleanup step.
1057
+
1058
+ ---
1059
+
1041
1060
  ### TC-DOC52: `get_doc_theme` scans body paragraph styles
1042
1061
  **Note:** `get_doc_theme` reads explicit per-paragraph and per-run styles from the document body. It returns data for AI-generated docs (where styles are set explicitly on runs); for standard docs whose styles are fully inherited from named style defaults it returns an empty dict.
1043
1062
 
@@ -21,6 +21,8 @@ Most infrastructure behaviours are verified by unit tests rather than live QA pr
21
21
  | DB recovery (issue #212) | Unit-tested in `tests/test_cache.py` `TestOpenFallback` — read-only file, read-only dir, and `:memory:` fallback all covered |
22
22
  | Tool doc generation (issue #94) | `scripts/gen_tool_docs.py` is a build-time/pre-commit script, not an MCP tool — no live prompt applies. Unit-tested in `tests/test_gen_tool_docs.py`: every registered tool is covered by a section and has a docstring, subset validation catches unknown tool names, and `main()` is idempotent on a second run |
23
23
  | TC-I21 (strict tool arg validation, issue #239) | ✅ Unit-tested in `tests/test_server.py::TestToolStrictArgs` (dummy tool + real `list_sheets`) and live-tested — see Result entry below |
24
+ | TC-I22 (`set_cache_ttl`/`get_cache_ttl`, issue #99) | Unit-tested in `tests/test_cache.py` (`set_ttl`/`get_ttl` on all 5 cache classes) — TTL change takes effect on the next lookup without a restart, and is readable back |
25
+ | TC-I23 (`CACHE_VALIDATE_MODIFIED_TIME`, issue #99) | Unit-tested in `tests/test_cache.py` (modified-time comparison in `_get_valid`, `get_modified_time` helper, `fetch_sheets` wiring). Live verification needs an edit path outside the MCP tools' own `mark_dirty` calls (which already invalidate immediately) — see TC-I23 below for the Playwright-based approach |
24
26
 
25
27
  ---
26
28
 
@@ -87,6 +89,64 @@ Restart the MCP server (`docker compose restart mcp-gee-sweet` or stop/start `uv
87
89
 
88
90
  ---
89
91
 
92
+ ### TC-I22: `set_cache_ttl`/`get_cache_ttl` — runtime TTL change takes effect without restart (issue #99)
93
+
94
+ **Prompt** (step 0 — record the starting TTL, to restore later)
95
+ > "What's the current cache TTL?"
96
+
97
+ **Prompt** (step 1 — warm the cache, default TTL)
98
+ > "List the sheets in {SPREADSHEET_ID}"
99
+
100
+ **Prompt** (step 2 — lower the TTL at runtime)
101
+ > "Set the cache TTL to 3 seconds"
102
+
103
+ **Prompt** (step 3 — confirm the new TTL is readable back)
104
+ > "What's the current cache TTL now?"
105
+
106
+ **Prompt** (step 4 — confirm the new TTL is honored)
107
+ > "Wait 5 seconds, then list the sheets in {SPREADSHEET_ID} again"
108
+
109
+ **Checks**
110
+ - Step 0 returns `{"ttl_seconds": 1800}` (the default, assuming no prior test left it changed)
111
+ - Step 2 returns `{"ttl_seconds": 3}`
112
+ - Step 3 returns `{"ttl_seconds": 3}` — `get_cache_ttl` reflects the change immediately
113
+ - Step 1's call is a cache miss (cold or expired from a prior test) — API fetch, result cached
114
+ - Step 4's call is a cache miss too, despite no server restart between steps — confirms the lowered TTL applied to the already-cached entry once evaluated, not just newly stored ones
115
+ - Restore the TTL after this test: `"Set the cache TTL to 1800"`
116
+
117
+ **Result (2026-07-08) ✅ PASS**
118
+ Ran all 5 steps live against `TEST_SPREADSHEET_ID`. Step 0: `get_cache_ttl` → `{"ttl_seconds": 1800}`. Step 1: `list_sheets` cache miss, warmed. Step 2: `set_cache_ttl(3)` → `{"ttl_seconds": 3}`; log emitted `WARNING mcp_gee_sweet.tools.cache Cache TTL changed process-wide 1800s -> 3s (affects all concurrent sessions)`. Step 3: `get_cache_ttl` → `{"ttl_seconds": 3}`, immediate readback confirmed. Step 4: after a 5s wait, `list_sheets` again — log showed `Cache TTL expired for <id>, marking dirty` followed by a fresh `Cached 4 sheets`, confirming the lowered TTL applied retroactively to the already-cached entry. Restored TTL to 1800 (`get_cache_ttl` confirmed).
119
+
120
+ ---
121
+
122
+ ### TC-I23: `CACHE_VALIDATE_MODIFIED_TIME` — external edit invalidates cache before TTL expires (issue #99)
123
+
124
+ **Background:** by default, a sheet-structure/data or doc-content cache hit is checked against the source file's live Drive `modifiedTime` before being served. This matters specifically for edits that don't go through this MCP session's own write tools (which already call `mark_dirty` immediately) — e.g. another Claude session, or a person editing the file directly in the Sheets/Docs UI.
125
+
126
+ **Setup**
127
+ 1. Warm the cache: "List the sheets in {SPREADSHEET_ID}"
128
+ 2. Using the browser (Playwright), open {SPREADSHEET_ID} in the Sheets UI and rename one sheet tab directly (not through any MCP tool) — this changes Drive's `modifiedTime` without calling `mark_dirty`.
129
+
130
+ **Prompt**
131
+ > "List the sheets in {SPREADSHEET_ID}"
132
+
133
+ **Checks**
134
+ - The renamed sheet's new title is reflected in the result, even though `CACHE_TTL` has not elapsed since step 1 — the `modifiedTime` mismatch invalidated the cache entry immediately
135
+ - Logs show a lightweight `files.get` call (`fields=modifiedTime`) preceding the decision, distinct from the fuller `spreadsheets.get` fetch that follows on the resulting miss
136
+
137
+ Note: Playwright is used here only as the mechanism to make an edit outside the MCP tool surface (step 2 of Setup) — the actual pass/fail check is a plain API-response comparison, not a visual confirmation. Doesn't fit the existing `Playwright: required` tag definition (visual mutation the API can't confirm), so left untagged; flagging in case this is a second, distinct case the tag convention should eventually cover.
138
+
139
+ **Cleanup:** rename the sheet back to its original name.
140
+
141
+ **Result (2026-07-08) ✅ PASS**
142
+ Warmed the structure cache via `list_sheets("BrandNew"...)`, then used Playwright to rename the "BrandNew" tab to "BrandNewRenamedQA" directly in the Sheets UI. Immediate `list_sheets` call (no wait, `CACHE_TTL` at default 1800s) returned the new name. Log showed `DEBUG mcp_gee_sweet.cache Source modified since cache for <id>, marking dirty` — the modifiedTime-mismatch path, distinct from the `Cache TTL expired` path — immediately followed by a fresh `Cached 4 sheets`. Renamed back via Playwright; confirmed reverted.
143
+
144
+ **Supplementary live check (2026-07-08) — cache-poisoning regression from code review, not a scripted TC:** the code review on this PR's first pass (before the `6d4a59b` fix) found that `_get_sheet_id` (backing ~11 write-path tools: `add_rows`, `rename_sheet`, `format_cells`, etc.) called `fetch_sheets()` without `drive_service`, storing the shared structure-cache row with `modified_time=None` — silently disabling staleness detection for *all* subsequent readers of that spreadsheet, including `list_sheets`. Verified the fix closes this end-to-end: `refresh_cache` (cold) → `freeze(sheet="EmptyRenamedQA")` (write-path call, cache MISS/STORE) → renamed "EmptyRenamedQA" back to "Empty" via Playwright → `list_sheets` (read-path, no explicit refresh) immediately returned `"Empty"`, with the log showing `Source modified since cache... marking dirty` right after the write-path's own store. Confirms the write-path cache entry now carries a valid `modified_time`, so it no longer poisons reads by other tools.
145
+
146
+ TC-I22/TC-I23 (issue #99) are the only mandatory live QA cases for this PR (see `git diff origin/develop...HEAD -- docs/qa/tests/`). No existing test case directly exercises the `_get_sheet_id`/write-path fix or `get_multiple_spreadsheet_summary`'s per-sheet `modified_time` fix — the supplementary check above covers the more severe of the two live; the `get_multiple_spreadsheet_summary` gap is lower-risk (confirmed correct by code trace + unit tests, not independently live-verified here).
147
+
148
+ ---
149
+
90
150
  ## Tool filtering (`ENABLED_TOOLS`)
91
151
 
92
152
  ### TC-I05: CLI flag — only specified tools registered
@@ -95,8 +95,8 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
95
95
  ### Tier 2 — Useful for structured work, plus defects surfaced since v0.8.1 _(target: [v0.9.0](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9))_
96
96
 
97
97
  **Defects** _(found after v0.8.1 shipped; too late for that release, don't warrant a standalone patch)_
98
- - [ ] 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 ([#248](https://github.com/khuisman/mcp-gee-sweet/issues/248))
99
- - [ ] `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 ([#277](https://github.com/khuisman/mcp-gee-sweet/issues/277))
98
+ - [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))
99
+ - [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))
100
100
 
101
101
  **Sheets**
102
102
  - [ ] `update_borders` — border style, width, color on a range ([#122](https://github.com/khuisman/mcp-gee-sweet/issues/122)) _(freema/mcp-gsheets)_
@@ -106,11 +106,11 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
106
106
  - [ ] `update_sheet_properties` — tab color, hide/show gridlines ([#126](https://github.com/khuisman/mcp-gee-sweet/issues/126)) _(freema/mcp-gsheets)_
107
107
  - [ ] `duplicate_sheet` — copy a sheet within the same spreadsheet ([#127](https://github.com/khuisman/mcp-gee-sweet/issues/127))
108
108
  - [ ] Partial (rich-text) hyperlinks in `update_cells` ([#89](https://github.com/khuisman/mcp-gee-sweet/issues/89))
109
- - [ ] `import_csv_to_sheet` — populate a spreadsheet from a local CSV file ([#187](https://github.com/khuisman/mcp-gee-sweet/issues/187))
109
+ - [x] `import_csv_to_sheet` — populate a spreadsheet from a local CSV file (PR #272) ([#187](https://github.com/khuisman/mcp-gee-sweet/issues/187))
110
110
 
111
111
  **Calendar**
112
- - [ ] `create_calendar` / `update_calendar` / `delete_calendar` — calendar lifecycle ([#156](https://github.com/khuisman/mcp-gee-sweet/issues/156))
113
- - [ ] `add_calendar_to_list` / `remove_calendar_from_list` — subscribe/unsubscribe ([#157](https://github.com/khuisman/mcp-gee-sweet/issues/157))
112
+ - [x] `create_calendar` / `update_calendar` / `delete_calendar` — calendar lifecycle (PR #266) ([#156](https://github.com/khuisman/mcp-gee-sweet/issues/156))
113
+ - [x] `add_calendar_to_list` / `remove_calendar_from_list` — subscribe/unsubscribe (PR #269) ([#157](https://github.com/khuisman/mcp-gee-sweet/issues/157))
114
114
  - [ ] Calendar ACL — share a calendar with users or groups ([#158](https://github.com/khuisman/mcp-gee-sweet/issues/158))
115
115
  - [ ] `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))
116
116
 
@@ -120,7 +120,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
120
120
  - [ ] Comments API — list, add, resolve doc comments ([#151](https://github.com/khuisman/mcp-gee-sweet/issues/151))
121
121
  - [ ] `create_named_range` / `create_bookmark` — anchor points for internal links ([#152](https://github.com/khuisman/mcp-gee-sweet/issues/152))
122
122
  - [ ] Rowspan emitter — AST `Cell.rowspan` field exists but emitter does not yet merge rows ([#195](https://github.com/khuisman/mcp-gee-sweet/issues/195))
123
- - [ ] Warn/error on mixed text + nested table in same cell ([#108](https://github.com/khuisman/mcp-gee-sweet/issues/108))
123
+ - [x] Warn/error on mixed text + nested table in same cell — shipped as the fuller `Cell.children: list[Run | Table]` ordered model, also fixing trailing-text-after-nested-table ordering ([#275](https://github.com/khuisman/mcp-gee-sweet/issues/275)) in the same PR (PR #276) ([#108](https://github.com/khuisman/mcp-gee-sweet/issues/108))
124
124
  - [ ] colspan/rowspan inside nested tables ([#109](https://github.com/khuisman/mcp-gee-sweet/issues/109))
125
125
  - [ ] `find_in_doc` — search doc text and return match locations, parallel to `find_in_spreadsheet`; removes the manual index math currently needed to retrofit links (or any style) onto text already in a doc, raised while fixing #248 ([#262](https://github.com/khuisman/mcp-gee-sweet/issues/262))
126
126
 
@@ -135,7 +135,8 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
135
135
  - [x] Drive Activity API — file change history ([#72](https://github.com/khuisman/mcp-gee-sweet/issues/72))
136
136
 
137
137
  **Infrastructure**
138
- - [ ] Cache reliability & configurability — runtime-configurable TTL, smarter invalidation for shared files ([#99](https://github.com/khuisman/mcp-gee-sweet/issues/99)); harden concurrent-session access — fail-open on cache read/write errors, `busy_timeout` ([#234](https://github.com/khuisman/mcp-gee-sweet/issues/234))
138
+ - [x] Harden concurrent-session access — fail-open on cache read/write errors, `busy_timeout` (PR #280) ([#234](https://github.com/khuisman/mcp-gee-sweet/issues/234))
139
+ - [x] Cache reliability & configurability — runtime-configurable TTL, smarter invalidation for shared files (PR #284) ([#99](https://github.com/khuisman/mcp-gee-sweet/issues/99))
139
140
  - [ ] Async tool execution — `asyncio.gather()` for parallel Google API calls; establishes the project's parallel-call pattern, which [#194](https://github.com/khuisman/mcp-gee-sweet/issues/194) should reuse rather than introducing its own `ThreadPoolExecutor` ([#183](https://github.com/khuisman/mcp-gee-sweet/issues/183))
140
141
 
141
142
  **Documentation**
@@ -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
- 90 tools. All tool names are lowercase with underscores — use these exact strings with `ENABLED_TOOLS` or `--include-tools`.
5
+ 92 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
 
@@ -160,6 +160,8 @@ Requires the `drive.activity.readonly` scope; uses the Drive Activity API v2.
160
160
 
161
161
  | Tool | Description | Key parameters |
162
162
  |---|---|---|
163
+ | `get_cache_ttl` | Read the cache TTL currently in effect for this running process. | — |
164
+ | `set_cache_ttl` | Change the cache time-to-live at runtime, without restarting the server. | `ttl_seconds` |
163
165
  | `refresh_cache` | Invalidate caches, forcing a fresh fetch on next use. | `spreadsheet_id?`, `doc_id?`, `folder_id?`, `calendar_id?` |
164
166
 
165
167
  Omit all parameters to flush the entire cache. See [Configuration](configuration.md#caching) for TTL and path settings.
@@ -26,8 +26,12 @@ ACCESS_LOG_FILE=/tmp/mcp-gee-sweet-access.log
26
26
  # PORT=8000
27
27
 
28
28
  # Cache
29
- # CACHE_TTL=1800
29
+ # CACHE_TTL=1800 # also adjustable at runtime via the set_cache_ttl tool, no restart needed
30
30
  # CACHE_DB_PATH=/tmp/mcp_gee_sweet.db
31
+ # Validate sheet/doc cache hits against Drive's modifiedTime before serving them
32
+ # (one extra Drive API call per lookup). Disable if that overhead isn't worth it
33
+ # for your usage pattern.
34
+ # CACHE_VALIDATE_MODIFIED_TIME=true
31
35
 
32
36
  # Safety cap on response size, in characters, for tools that can return large inline
33
37
  # payloads (get_sheet_data, get_multiple_sheet_data, find_in_spreadsheet,