mcp-gee-sweet 0.7.0.dev67__tar.gz → 0.7.0.dev68__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 (135) hide show
  1. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/PKG-INFO +1 -1
  2. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/configuration.md +2 -2
  3. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/calendar.md +138 -0
  4. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/tools.md +4 -1
  5. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/scripts/gen_tool_docs.py +3 -0
  6. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/calendar.py +134 -0
  7. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_calendar.py +186 -0
  8. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.claude/commands/prep-for-pr.md +0 -0
  9. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.dockerignore +0 -0
  10. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/dependabot.yml +0 -0
  14. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/workflows/ci.yml +0 -0
  15. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/workflows/docs.yml +0 -0
  16. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/workflows/publish-dev.yml +0 -0
  17. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.github/workflows/release.yml +0 -0
  18. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.gitignore +0 -0
  19. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.pre-commit-config.yaml +0 -0
  20. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/.python-version +0 -0
  21. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/CHANGELOG.md +0 -0
  22. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/CLAUDE.md +0 -0
  23. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/CODE_OF_CONDUCT.md +0 -0
  24. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/CONTRIBUTING.md +0 -0
  25. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/Dockerfile +0 -0
  26. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/LICENSE +0 -0
  27. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/Makefile +0 -0
  28. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/README.md +0 -0
  29. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/SECURITY.md +0 -0
  30. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docker-compose.yml +0 -0
  31. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/auth.md +0 -0
  32. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/client-setup.md +0 -0
  33. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-chart-theming.md +0 -0
  34. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-composite-tools.md +0 -0
  35. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-docs-formatting.md +0 -0
  36. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-fork.md +0 -0
  37. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  38. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-publishing.md +0 -0
  39. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  40. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/decision-testing.md +0 -0
  41. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/decisions/index.md +0 -0
  42. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/design/docs-ast-pipeline.md +0 -0
  43. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/design/effectiveformat-spike.md +0 -0
  44. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/design/index.md +0 -0
  45. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/design/markdown-support.md +0 -0
  46. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/design/phase3-theme-system.md +0 -0
  47. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/design.md +0 -0
  48. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/index.md +0 -0
  49. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/known-limitations.md +0 -0
  50. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/notes-read.md +0 -0
  51. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/README.md +0 -0
  52. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  53. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  54. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  55. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  56. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/fixtures.template.md +0 -0
  57. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/operations.yaml +0 -0
  58. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/playwright_oauth.md +0 -0
  59. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/.gitkeep +0 -0
  60. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-02.md +0 -0
  61. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-14.md +0 -0
  62. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-17.md +0 -0
  63. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-18.md +0 -0
  64. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  65. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  66. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-06-28.md +0 -0
  67. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-07-04-docs.md +0 -0
  68. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  69. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-07-04-infra.md +0 -0
  70. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  71. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/results/2026-07-04.md +0 -0
  72. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/retro-v0.8.0.md +0 -0
  73. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/run.md +0 -0
  74. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/runs/README.md +0 -0
  75. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/runs/v0.8.0.md +0 -0
  76. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/runs/v0.8.1.md +0 -0
  77. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/setup.md +0 -0
  78. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/docs.md +0 -0
  79. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/drive.md +0 -0
  80. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/infra.md +0 -0
  81. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/sheets_charts.md +0 -0
  82. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/sheets_mgmt.md +0 -0
  83. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/sheets_read.md +0 -0
  84. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa/tests/sheets_write.md +0 -0
  85. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/qa-checklist.md +0 -0
  86. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/docs/roadmap.md +0 -0
  87. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/mkdocs.yml +0 -0
  88. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/pyproject.toml +0 -0
  89. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/scripts/oauth_setup.py +0 -0
  90. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/.env.template +0 -0
  91. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/__init__.py +0 -0
  92. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/auth.py +0 -0
  93. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/cache.py +0 -0
  94. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/server.py +0 -0
  95. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  96. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/cache.py +0 -0
  97. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  98. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  99. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  100. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  101. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
  102. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  103. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  104. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  105. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  106. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  107. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  108. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  109. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  110. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  111. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  112. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  113. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  114. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  115. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/__init__.py +0 -0
  116. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/drive/__init__.py +0 -0
  117. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/drive/test_activity.py +0 -0
  118. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/drive/test_files.py +0 -0
  119. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/drive/test_sharing.py +0 -0
  120. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/drive/test_transfer.py +0 -0
  121. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/sheets/__init__.py +0 -0
  122. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/sheets/test_data.py +0 -0
  123. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/sheets/test_helpers.py +0 -0
  124. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/sheets/test_structure.py +0 -0
  125. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_auth.py +0 -0
  126. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_cache.py +0 -0
  127. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_docs_content.py +0 -0
  128. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_docs_core.py +0 -0
  129. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_docs_layout.py +0 -0
  130. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_docs_style.py +0 -0
  131. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_docs_tables.py +0 -0
  132. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_gen_tool_docs.py +0 -0
  133. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_response_limits.py +0 -0
  134. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/tests/test_server.py +0 -0
  135. {mcp_gee_sweet-0.7.0.dev67 → mcp_gee_sweet-0.7.0.dev68}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-gee-sweet
3
- Version: 0.7.0.dev67
3
+ Version: 0.7.0.dev68
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
@@ -69,7 +69,7 @@ Access logs are emitted at `INFO` level — they appear when `DEBUG_LEVEL` is `D
69
69
 
70
70
  <!-- Auto-generated by `scripts/gen_tool_docs.py` — do not edit by hand, your changes will be overwritten on the next commit. -->
71
71
 
72
- By default all 84 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.
72
+ By default all 87 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
73
 
74
74
  ```bash
75
75
  # Environment variable
@@ -100,7 +100,7 @@ create_doc,get_doc_content,get_doc_structure,write_doc_content,insert_doc_text,i
100
100
 
101
101
  **Calendar only:**
102
102
  ```
103
- list_calendars,get_calendar,list_events,get_event,create_event,update_event,delete_event,find_free_slots
103
+ list_calendars,get_calendar,create_calendar,update_calendar,delete_calendar,list_events,get_event,create_event,update_event,delete_event,find_free_slots
104
104
  ```
105
105
 
106
106
  See [Tools](tools.md) for the full list of tool names.
@@ -105,6 +105,144 @@ Fixtures: see [`docs/qa/setup.md`](../setup.md). Substitute your `{CALENDAR_ID}`
105
105
 
106
106
  ---
107
107
 
108
+ ## `create_calendar`
109
+
110
+ ### TC-CAL44: Create a secondary calendar ⚠️ destructive
111
+
112
+ **Prompt**
113
+ > "Create a new calendar called 'mcp-gee-sweet-qa-lifecycle-test' with description 'QA scratch calendar' and timezone 'America/Los_Angeles'"
114
+
115
+ **Checks**
116
+ - Returns `id`, `summary`, `description`, `time_zone`
117
+ - `summary` is `mcp-gee-sweet-qa-lifecycle-test`
118
+ - `time_zone` is `America/Los_Angeles`
119
+ - The new calendar appears in a follow-up `list_calendars` call (cache invalidated)
120
+
121
+ **Result (2026-07-05) ✅** — Created via OAuth; response had `summary`, `description`, and `time_zone: "America/Los_Angeles"` exactly as requested. Confirmed present in a follow-up `list_calendars` call.
122
+
123
+ ---
124
+
125
+ ### TC-CAL45: Create with only summary
126
+
127
+ **Prompt**
128
+ > "Create a new calendar called 'mcp-gee-sweet-qa-minimal'"
129
+
130
+ **Checks**
131
+ - Succeeds with only `summary` provided
132
+ - `description` is `null`/absent
133
+ - `time_zone` reflects the account default
134
+
135
+ **Result (2026-07-05) ✅** — Created with only `summary` provided. `description: null`. `time_zone: "UTC"` (account default rather than the creator's local timezone — noted for documentation purposes). Confirmed present in a follow-up `list_calendars` call.
136
+
137
+ ---
138
+
139
+ ## `update_calendar`
140
+
141
+ ### TC-CAL46: Rename and change timezone ⚠️ destructive
142
+
143
+ **Setup:** use the calendar created in TC-CAL44
144
+
145
+ **Prompt**
146
+ > "Update calendar 'mcp-gee-sweet-qa-lifecycle-test' — rename it to 'mcp-gee-sweet-qa-renamed' and set its timezone to 'America/New_York'"
147
+
148
+ **Checks**
149
+ - `summary` is `mcp-gee-sweet-qa-renamed`
150
+ - `time_zone` is `America/New_York`
151
+ - `description` unchanged from TC-CAL44
152
+ - `get_calendar` on the same ID reflects the new values
153
+
154
+ **Result (2026-07-05) ✅** — Patched with `summary: "mcp-gee-sweet-qa-renamed"` and `timezone: "America/New_York"`. Response reflected both changes; `description` unchanged from TC-CAL44 (`"QA scratch calendar"`).
155
+
156
+ ---
157
+
158
+ ### TC-CAL47: Change color only ⚠️ destructive
159
+
160
+ **Setup:** use the calendar from TC-CAL44/46
161
+
162
+ **Prompt**
163
+ > "Change the color of calendar 'mcp-gee-sweet-qa-renamed' to color ID '5'"
164
+
165
+ **Checks**
166
+ - `color_id` in the response is `5`
167
+ - `summary`/`description`/`time_zone` unchanged (verify with `get_calendar`)
168
+ - Confirms the color patch goes through `calendarList().patch()`, not `calendars().patch()`
169
+
170
+ **Result (2026-07-05) ✅** — Patched `color_id="5"` only (no summary/description/timezone). Response had `color_id: "5"` with `summary`, `description`, `time_zone` all unchanged from TC-CAL46 — confirms the tool fell back to `calendars().get()` for the base fields and routed color through `calendarList().patch()`.
171
+
172
+ ---
173
+
174
+ ### TC-CAL48: No fields provided
175
+
176
+ **Prompt**
177
+ > "Update calendar {CALENDAR_ID} with no changes — just fetch its current state through update_calendar"
178
+
179
+ **Checks**
180
+ - Returns current calendar metadata unchanged
181
+ - `color_id` is `null` (no color patch attempted)
182
+ - No API error
183
+
184
+ **Result (2026-07-05) ✅** — Called with `calendar_id="primary"` and no other fields. Returned the account's real primary calendar summary/timezone unchanged, `color_id: null`. Confirms the no-op path uses `calendars().get()` without issuing any patch.
185
+
186
+ ---
187
+
188
+ ### TC-CAL49: Non-existent calendar ID
189
+
190
+ **Prompt**
191
+ > "Update calendar 'totally-invalid-cal-id@example.com' — rename it to 'Nope'"
192
+
193
+ **Checks**
194
+ - Returns `{"error": "..."}` — not a top-level exception
195
+
196
+ **Result (2026-07-05) ✅** — Returned `{"error": "<HttpError 404 ... 'reason': 'notFound' ...>"}`. No exception raised.
197
+
198
+ ---
199
+
200
+ ## `delete_calendar`
201
+
202
+ ### TC-CAL50: Delete a calendar ⚠️ destructive
203
+
204
+ **Setup:** use the calendar created in TC-CAL44 (after TC-CAL46/47 updates)
205
+
206
+ **Prompt**
207
+ > "Delete the calendar 'mcp-gee-sweet-qa-renamed'"
208
+
209
+ **Checks**
210
+ - Returns `{"calendar_id": "...", "action": "deleted"}`
211
+ - Calendar no longer appears in a follow-up `list_calendars` call
212
+ - `get_calendar` on the deleted ID returns an error
213
+
214
+ **Result (2026-07-05) ✅** — Returned `{"calendar_id": "...", "action": "deleted"}`. Confirmed absent from a follow-up `list_calendars` call. `get_calendar` on the deleted ID returned a 404 `notFound` error.
215
+
216
+ ---
217
+
218
+ ### TC-CAL51: Delete the minimal test calendar ⚠️ destructive
219
+
220
+ **Setup:** use the calendar created in TC-CAL45
221
+
222
+ **Prompt**
223
+ > "Delete the calendar 'mcp-gee-sweet-qa-minimal'"
224
+
225
+ **Checks**
226
+ - Returns `{"calendar_id": "...", "action": "deleted"}`
227
+ - Cleans up the fixture so it doesn't linger in the account's calendar list
228
+
229
+ **Result (2026-07-05) ✅** — Returned `{"calendar_id": "...", "action": "deleted"}`. Confirmed removed from the account's `list_calendars` output alongside TC-CAL50's cleanup.
230
+
231
+ ---
232
+
233
+ ### TC-CAL52: Non-existent calendar ID
234
+
235
+ **Prompt**
236
+ > "Delete the calendar 'totally-invalid-cal-id@example.com'"
237
+
238
+ **Checks**
239
+ - Returns `{"error": "..."}` — not a top-level exception
240
+ - No side effects
241
+
242
+ **Result (2026-07-05) ✅** — Returned `{"error": "<HttpError 404 ... 'reason': 'notFound' ...>"}`. No side effects.
243
+
244
+ ---
245
+
108
246
  ## `list_events`
109
247
 
110
248
  ### TC-CAL09: No time filters — upcoming events
@@ -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
- 84 tools. All tool names are lowercase with underscores — use these exact strings with `ENABLED_TOOLS` or `--include-tools`.
5
+ 87 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
 
@@ -141,6 +141,9 @@ Requires the `drive.activity.readonly` scope; uses the Drive Activity API v2.
141
141
  |---|---|---|
142
142
  | `list_calendars` | List all calendars accessible to the authenticated user. | — |
143
143
  | `get_calendar` | Fetch metadata for a single calendar. | `calendar_id` |
144
+ | `create_calendar` | Create a new secondary calendar owned by the authenticated user. | `summary`, `description?`, `timezone?` |
145
+ | `update_calendar` | Update fields on an existing calendar using patch semantics. Only the fields you provide are changed; omitted fields are left as-is. | `calendar_id`, `summary?`, `description?`, `timezone?`, `color_id?` |
146
+ | `delete_calendar` | Permanently delete a calendar. For calendars you own, this deletes the calendar entirely for all users it is shared with — use with caution. To only stop seeing a calendar in your own list, use remove_calendar_from_list instead. | `calendar_id` |
144
147
  | `list_events` | List events in a calendar. | `calendar_id`, `time_min?`, `time_max?`, `query?`, `max_results?`, `expand_recurring?` |
145
148
  | `get_event` | Fetch a single event by calendar ID and event ID. | `calendar_id`, `event_id` |
146
149
  | `create_event` | Create a new event in a calendar. | `calendar_id`, `summary`, `start`, `end`, `description?`, `location?`, `attendees?`, `timezone?`, `recurrence?` |
@@ -215,6 +215,9 @@ SUBSETS = [
215
215
  [
216
216
  "list_calendars",
217
217
  "get_calendar",
218
+ "create_calendar",
219
+ "update_calendar",
220
+ "delete_calendar",
218
221
  "list_events",
219
222
  "get_event",
220
223
  "create_event",
@@ -73,6 +73,140 @@ def register(tool):
73
73
  cache.store(calendar_id, result)
74
74
  return result
75
75
 
76
+ @tool(annotations=ToolAnnotations(title="Create Calendar", destructiveHint=True))
77
+ def create_calendar(
78
+ summary: str,
79
+ description: str | None = None,
80
+ timezone: str | None = None,
81
+ ctx: Context = None,
82
+ ) -> dict[str, Any]:
83
+ """
84
+ Create a new secondary calendar owned by the authenticated user.
85
+
86
+ Args:
87
+ summary: Calendar title.
88
+ description: Optional calendar description.
89
+ timezone: IANA timezone name (e.g. 'America/Los_Angeles'). Defaults to
90
+ the account's timezone if omitted.
91
+
92
+ Returns:
93
+ Created calendar: id, summary, description, time_zone.
94
+ """
95
+ lc = ctx.request_context.lifespan_context
96
+
97
+ body: dict[str, Any] = {"summary": summary}
98
+ if description:
99
+ body["description"] = description
100
+ if timezone:
101
+ body["timeZone"] = timezone
102
+
103
+ try:
104
+ c = lc.calendar_service.calendars().insert(body=body).execute()
105
+ except Exception as e:
106
+ return {"error": str(e)}
107
+
108
+ lc.calendar_cache.mark_dirty(c["id"])
109
+ logger.debug("Created calendar %s", c["id"])
110
+ return {
111
+ "id": c["id"],
112
+ "summary": c.get("summary", ""),
113
+ "description": c.get("description"),
114
+ "time_zone": c.get("timeZone"),
115
+ }
116
+
117
+ @tool(annotations=ToolAnnotations(title="Update Calendar", destructiveHint=True))
118
+ def update_calendar(
119
+ calendar_id: str,
120
+ summary: str | None = None,
121
+ description: str | None = None,
122
+ timezone: str | None = None,
123
+ color_id: str | None = None,
124
+ ctx: Context = None,
125
+ ) -> dict[str, Any]:
126
+ """
127
+ Update fields on an existing calendar using patch semantics. Only the
128
+ fields you provide are changed; omitted fields are left as-is.
129
+
130
+ Note: color_id is set via the authenticated user's calendar list entry
131
+ (calendarList().patch()), not the calendar resource itself — it is a
132
+ per-user setting, not a property of the calendar shared with others.
133
+
134
+ Args:
135
+ calendar_id: The calendar ID, or 'primary'.
136
+ summary: New calendar title.
137
+ description: New calendar description.
138
+ timezone: New IANA timezone name (e.g. 'America/Los_Angeles').
139
+ color_id: New color ID from the palette returned by the Calendar API's
140
+ colors().get() endpoint (predefined IDs, typically '1'-'24').
141
+
142
+ Returns:
143
+ Updated calendar: id, summary, description, time_zone, color_id.
144
+ """
145
+ lc = ctx.request_context.lifespan_context
146
+
147
+ patch: dict[str, Any] = {}
148
+ if summary is not None:
149
+ patch["summary"] = summary
150
+ if description is not None:
151
+ patch["description"] = description
152
+ if timezone is not None:
153
+ patch["timeZone"] = timezone
154
+
155
+ try:
156
+ if patch:
157
+ c = (
158
+ lc.calendar_service.calendars()
159
+ .patch(calendarId=calendar_id, body=patch)
160
+ .execute()
161
+ )
162
+ else:
163
+ c = lc.calendar_service.calendars().get(calendarId=calendar_id).execute()
164
+
165
+ result_color_id = None
166
+ if color_id is not None:
167
+ list_entry = (
168
+ lc.calendar_service.calendarList()
169
+ .patch(calendarId=calendar_id, body={"colorId": color_id})
170
+ .execute()
171
+ )
172
+ result_color_id = list_entry.get("colorId")
173
+ except Exception as e:
174
+ return {"error": str(e)}
175
+
176
+ lc.calendar_cache.mark_dirty(calendar_id)
177
+ logger.debug("Updated calendar %s", calendar_id)
178
+ return {
179
+ "id": c.get("id", calendar_id),
180
+ "summary": c.get("summary", ""),
181
+ "description": c.get("description"),
182
+ "time_zone": c.get("timeZone"),
183
+ "color_id": result_color_id,
184
+ }
185
+
186
+ @tool(annotations=ToolAnnotations(title="Delete Calendar", destructiveHint=True))
187
+ def delete_calendar(calendar_id: str, ctx: Context = None) -> dict[str, Any]:
188
+ """
189
+ Permanently delete a calendar. For calendars you own, this deletes the
190
+ calendar entirely for all users it is shared with — use with caution.
191
+ To only stop seeing a calendar in your own list, use
192
+ remove_calendar_from_list instead.
193
+
194
+ Args:
195
+ calendar_id: The calendar ID to delete.
196
+
197
+ Returns:
198
+ Confirmation with calendar_id and action 'deleted'.
199
+ """
200
+ lc = ctx.request_context.lifespan_context
201
+ try:
202
+ lc.calendar_service.calendars().delete(calendarId=calendar_id).execute()
203
+ except Exception as e:
204
+ return {"error": str(e)}
205
+
206
+ lc.calendar_cache.mark_dirty(calendar_id)
207
+ logger.debug("Deleted calendar %s", calendar_id)
208
+ return {"calendar_id": calendar_id, "action": "deleted"}
209
+
76
210
  @tool(annotations=ToolAnnotations(title="List Events", readOnlyHint=True))
77
211
  def list_events(
78
212
  calendar_id: str,
@@ -73,6 +73,192 @@ class TestListCalendars:
73
73
  cache.store_list.assert_called_once_with(result)
74
74
 
75
75
 
76
+ class TestCreateCalendar:
77
+ """create_calendar inserts a new secondary calendar and invalidates the list cache."""
78
+
79
+ def test_builds_body_from_provided_fields_and_maps_response(self):
80
+ """summary/description/timezone go in the insert body; response is field-mapped."""
81
+ cal_svc = MagicMock()
82
+ cal_svc.calendars.return_value.insert.return_value.execute.return_value = {
83
+ "id": "new-cal-1",
84
+ "summary": "Team Events",
85
+ "description": "Shared team calendar",
86
+ "timeZone": "America/Los_Angeles",
87
+ }
88
+ cache = MagicMock()
89
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
90
+
91
+ result = _cal_tools["create_calendar"](
92
+ summary="Team Events",
93
+ description="Shared team calendar",
94
+ timezone="America/Los_Angeles",
95
+ ctx=ctx,
96
+ )
97
+
98
+ body = cal_svc.calendars.return_value.insert.call_args.kwargs["body"]
99
+ assert body == {
100
+ "summary": "Team Events",
101
+ "description": "Shared team calendar",
102
+ "timeZone": "America/Los_Angeles",
103
+ }
104
+ assert result["id"] == "new-cal-1"
105
+ assert result["time_zone"] == "America/Los_Angeles"
106
+
107
+ def test_omits_optional_fields_from_body_when_not_provided(self):
108
+ """When description/timezone are omitted, the insert body must only have summary."""
109
+ cal_svc = MagicMock()
110
+ cal_svc.calendars.return_value.insert.return_value.execute.return_value = {
111
+ "id": "new-cal-2",
112
+ "summary": "Minimal",
113
+ }
114
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=MagicMock())
115
+
116
+ _cal_tools["create_calendar"](summary="Minimal", ctx=ctx)
117
+
118
+ body = cal_svc.calendars.return_value.insert.call_args.kwargs["body"]
119
+ assert body == {"summary": "Minimal"}
120
+
121
+ def test_marks_cache_dirty_with_new_calendar_id(self):
122
+ """calendar_cache.mark_dirty must be called with the newly created calendar's id."""
123
+ cal_svc = MagicMock()
124
+ cal_svc.calendars.return_value.insert.return_value.execute.return_value = {
125
+ "id": "new-cal-3",
126
+ "summary": "Test",
127
+ }
128
+ cache = MagicMock()
129
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
130
+
131
+ _cal_tools["create_calendar"](summary="Test", ctx=ctx)
132
+
133
+ cache.mark_dirty.assert_called_once_with("new-cal-3")
134
+
135
+ def test_api_error_returns_error_dict_without_marking_cache_dirty(self):
136
+ """When calendars().insert() raises, result must be {"error": ...} and cache stays clean."""
137
+ cal_svc = MagicMock()
138
+ cal_svc.calendars.return_value.insert.return_value.execute.side_effect = Exception(
139
+ "quotaExceeded"
140
+ )
141
+ cache = MagicMock()
142
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
143
+
144
+ result = _cal_tools["create_calendar"](summary="Test", ctx=ctx)
145
+
146
+ assert "error" in result
147
+ cache.mark_dirty.assert_not_called()
148
+
149
+
150
+ class TestUpdateCalendar:
151
+ """update_calendar uses patch semantics for core fields and calendarList for color."""
152
+
153
+ def test_only_summary_in_body_when_only_summary_provided(self):
154
+ """If only summary is given, the patch body must not include description or timeZone."""
155
+ cal_svc = MagicMock()
156
+ cal_svc.calendars.return_value.patch.return_value.execute.return_value = {
157
+ "id": "cal-1",
158
+ "summary": "Renamed",
159
+ }
160
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=MagicMock())
161
+
162
+ _cal_tools["update_calendar"](calendar_id="cal-1", summary="Renamed", ctx=ctx)
163
+
164
+ body = cal_svc.calendars.return_value.patch.call_args.kwargs["body"]
165
+ assert body == {"summary": "Renamed"}
166
+
167
+ def test_color_id_patches_calendar_list_not_calendars_resource(self):
168
+ """color_id must go through calendarList().patch(), not calendars().patch()."""
169
+ cal_svc = MagicMock()
170
+ cal_svc.calendars.return_value.get.return_value.execute.return_value = {
171
+ "id": "cal-1",
172
+ "summary": "Existing",
173
+ }
174
+ cal_svc.calendarList.return_value.patch.return_value.execute.return_value = {"colorId": "5"}
175
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=MagicMock())
176
+
177
+ result = _cal_tools["update_calendar"](calendar_id="cal-1", color_id="5", ctx=ctx)
178
+
179
+ cal_svc.calendars.return_value.patch.assert_not_called()
180
+ cal_svc.calendarList.return_value.patch.assert_called_once_with(
181
+ calendarId="cal-1", body={"colorId": "5"}
182
+ )
183
+ assert result["color_id"] == "5"
184
+
185
+ def test_no_fields_provided_falls_back_to_get_without_patching(self):
186
+ """With no fields set, the tool must fetch via calendars().get(), not patch()."""
187
+ cal_svc = MagicMock()
188
+ cal_svc.calendars.return_value.get.return_value.execute.return_value = {
189
+ "id": "cal-1",
190
+ "summary": "Unchanged",
191
+ }
192
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=MagicMock())
193
+
194
+ result = _cal_tools["update_calendar"](calendar_id="cal-1", ctx=ctx)
195
+
196
+ cal_svc.calendars.return_value.patch.assert_not_called()
197
+ cal_svc.calendars.return_value.get.assert_called_once_with(calendarId="cal-1")
198
+ assert result["summary"] == "Unchanged"
199
+ assert result["color_id"] is None
200
+
201
+ def test_marks_cache_dirty_with_correct_calendar_id(self):
202
+ """After a successful update, calendar_cache.mark_dirty is called with the calendar_id."""
203
+ cal_svc = MagicMock()
204
+ cal_svc.calendars.return_value.patch.return_value.execute.return_value = {
205
+ "id": "cal-2",
206
+ "summary": "Renamed",
207
+ }
208
+ cache = MagicMock()
209
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
210
+
211
+ _cal_tools["update_calendar"](calendar_id="cal-2", summary="Renamed", ctx=ctx)
212
+
213
+ cache.mark_dirty.assert_called_once_with("cal-2")
214
+
215
+ def test_api_error_returns_error_dict_without_marking_cache_dirty(self):
216
+ """When calendars().patch() raises, result must be {"error": ...} and cache stays clean."""
217
+ cal_svc = MagicMock()
218
+ cal_svc.calendars.return_value.patch.return_value.execute.side_effect = Exception(
219
+ "notFound"
220
+ )
221
+ cache = MagicMock()
222
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
223
+
224
+ result = _cal_tools["update_calendar"](
225
+ calendar_id="cal-missing", summary="Renamed", ctx=ctx
226
+ )
227
+
228
+ assert "error" in result
229
+ cache.mark_dirty.assert_not_called()
230
+
231
+
232
+ class TestDeleteCalendar:
233
+ """delete_calendar returns a structured confirmation and invalidates the cache."""
234
+
235
+ def test_success_returns_confirmation_dict_and_marks_cache_dirty(self):
236
+ """Result must be {calendar_id, action='deleted'} and cache is dirtied."""
237
+ cal_svc = MagicMock()
238
+ cal_svc.calendars.return_value.delete.return_value.execute.return_value = None
239
+ cache = MagicMock()
240
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
241
+
242
+ result = _cal_tools["delete_calendar"](calendar_id="cal-1", ctx=ctx)
243
+
244
+ assert result == {"calendar_id": "cal-1", "action": "deleted"}
245
+ cache.mark_dirty.assert_called_once_with("cal-1")
246
+
247
+ def test_api_error_returns_error_dict_without_marking_cache_dirty(self):
248
+ """If delete raises, result has 'error' key and the cache must not be dirtied."""
249
+ cal_svc = MagicMock()
250
+ cal_svc.calendars.return_value.delete.return_value.execute.side_effect = Exception(
251
+ "Not Found"
252
+ )
253
+ cache = MagicMock()
254
+ ctx = _make_ctx(calendar_service=cal_svc, calendar_cache=cache)
255
+
256
+ result = _cal_tools["delete_calendar"](calendar_id="cal-missing", ctx=ctx)
257
+
258
+ assert "error" in result
259
+ cache.mark_dirty.assert_not_called()
260
+
261
+
76
262
  class TestCreateEvent:
77
263
  """create_event detects all-day events, passes attendees, and invalidates the cache."""
78
264