mcp-gee-sweet 0.7.0.dev69__tar.gz → 0.7.0.dev72__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/CLAUDE.md +1 -1
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/configuration.md +3 -3
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/calendar.md +107 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/drive.md +83 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/tools.md +4 -1
- mcp_gee_sweet-0.7.0.dev72/pyrightconfig.json +4 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/scripts/gen_tool_docs.py +17 -2
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/calendar.py +82 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/files.py +154 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_files.py +224 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_calendar.py +141 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.dockerignore +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.gitignore +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/.python-version +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/Dockerfile +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/LICENSE +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/Makefile +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/design.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/docs/roadmap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/uv.lock +0 -0
|
@@ -34,7 +34,7 @@ Logic is split across `src/mcp_gee_sweet/`: `server.py` (MCP setup, tool decorat
|
|
|
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
35
|
- `tools/sheets/structure.py` — sheet structure (`list_sheets`, `copy_sheet`, `rename_sheet`, `create_sheet`, `delete_sheet`, `add_rows`, `add_columns`, `delete_rows`, `delete_columns`, `format_cells`, `merge_cells`, `unmerge_cells`, `freeze`, `sort_range`, `add_chart`)
|
|
36
36
|
- `tools/sheets/helpers.py` — A1 notation helpers (`_parse_a1_notation`, `_column_index_to_letter`, etc.)
|
|
37
|
-
- `tools/drive/files.py` — file/folder operations (`create_spreadsheet`, `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.)
|
|
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`)
|
|
39
39
|
- `tools/drive/transfer.py` — upload/download/sync/export/revisions
|
|
40
40
|
- `tools/drive/activity.py` — `list_file_activity` (Drive Activity API v2; requires `drive.activity.readonly` scope)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev72
|
|
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
|
|
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
73
|
|
|
74
74
|
```bash
|
|
75
75
|
# Environment variable
|
|
@@ -90,7 +90,7 @@ get_sheet_data,get_sheet_formulas,get_multiple_sheet_data,get_multiple_spreadshe
|
|
|
90
90
|
|
|
91
91
|
**Sheets read + write:**
|
|
92
92
|
```
|
|
93
|
-
get_sheet_data,get_sheet_formulas,get_multiple_sheet_data,get_multiple_spreadsheet_summary,find_in_spreadsheet,list_sheets,list_spreadsheets,update_cells,batch_update_cells,create_sheet,rename_sheet,add_rows,add_columns
|
|
93
|
+
get_sheet_data,get_sheet_formulas,get_multiple_sheet_data,get_multiple_spreadsheet_summary,find_in_spreadsheet,list_sheets,list_spreadsheets,update_cells,batch_update_cells,create_sheet,rename_sheet,add_rows,add_columns,import_csv_to_sheet
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
**Docs only:**
|
|
@@ -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,create_calendar,update_calendar,delete_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,add_calendar_to_list,remove_calendar_from_list,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.
|
|
@@ -243,6 +243,113 @@ Fixtures: see [`docs/qa/setup.md`](../setup.md). Substitute your `{CALENDAR_ID}`
|
|
|
243
243
|
|
|
244
244
|
---
|
|
245
245
|
|
|
246
|
+
## `add_calendar_to_list`
|
|
247
|
+
|
|
248
|
+
> **Note on test targets:** subscribing/unsubscribing only makes sense for a calendar you don't own — Google's API rejects unsubscribing from a calendar you're the owner of (see TC-CAL59). Public holiday calendars (e.g. `en.japanese#holiday@group.v.calendar.google.com`, `en.canadian#holiday@group.v.calendar.google.com`) are convenient, safe, reversible targets: not owned by any test account, freely subscribable, and already used elsewhere in this account for `en.usa#holiday@group.v.calendar.google.com`.
|
|
249
|
+
|
|
250
|
+
### TC-CAL53: Subscribe to an existing calendar ⚠️ destructive
|
|
251
|
+
|
|
252
|
+
**Setup:** a public calendar not currently in your list, e.g. `en.japanese#holiday@group.v.calendar.google.com` (Holidays in Japan)
|
|
253
|
+
|
|
254
|
+
**Prompt**
|
|
255
|
+
> "Subscribe me to calendar 'en.japanese#holiday@group.v.calendar.google.com'"
|
|
256
|
+
|
|
257
|
+
**Checks**
|
|
258
|
+
- Returns `id`, `summary`, `time_zone`, `access_role`, `primary`, `color_id`
|
|
259
|
+
- Calendar now appears in a follow-up `list_calendars` call (cache invalidated)
|
|
260
|
+
|
|
261
|
+
**Result (2026-07-05) ✅** — Response had `summary: "Holidays in Japan"`, `access_role: "reader"`, `color_id: "8"` (Google's default color for this public calendar). Confirmed present in a follow-up `list_calendars` call.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
### TC-CAL54: Subscribe with a color ⚠️ destructive
|
|
266
|
+
|
|
267
|
+
**Setup:** another public calendar not yet in your list, e.g. `en.canadian#holiday@group.v.calendar.google.com` (Holidays in Canada)
|
|
268
|
+
|
|
269
|
+
**Prompt**
|
|
270
|
+
> "Subscribe me to calendar 'en.canadian#holiday@group.v.calendar.google.com' with color ID '7'"
|
|
271
|
+
|
|
272
|
+
**Checks**
|
|
273
|
+
- `color_id` in the response is `7`
|
|
274
|
+
- Calendar appears in `list_calendars`
|
|
275
|
+
|
|
276
|
+
**Result (2026-07-05) ✅** — Response had `color_id: "7"` and `summary: "Holidays in Canada"`. Confirmed present in `list_calendars`.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### TC-CAL55: Non-existent calendar ID
|
|
281
|
+
|
|
282
|
+
**Prompt**
|
|
283
|
+
> "Subscribe me to calendar 'totally-invalid-cal-id@example.com'"
|
|
284
|
+
|
|
285
|
+
**Checks**
|
|
286
|
+
- Returns `{"error": "..."}` — not a top-level exception
|
|
287
|
+
- No entry added to the calendar list
|
|
288
|
+
|
|
289
|
+
**Result (2026-07-05) ✅** — Returned `{"error": "<HttpError 404 ... 'reason': 'notFound' ...>"}`.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## `remove_calendar_from_list`
|
|
294
|
+
|
|
295
|
+
### TC-CAL56: Unsubscribe from a calendar ⚠️ destructive
|
|
296
|
+
|
|
297
|
+
**Setup:** use the calendar subscribed to in TC-CAL53
|
|
298
|
+
|
|
299
|
+
**Prompt**
|
|
300
|
+
> "Remove calendar 'en.japanese#holiday@group.v.calendar.google.com' from my calendar list"
|
|
301
|
+
|
|
302
|
+
**Checks**
|
|
303
|
+
- Returns `{"calendar_id": "...", "action": "removed_from_list"}`
|
|
304
|
+
- Calendar no longer appears in a follow-up `list_calendars` call
|
|
305
|
+
|
|
306
|
+
**Result (2026-07-05) ✅** — Returned `{"calendar_id": "en.japanese#holiday@group.v.calendar.google.com", "action": "removed_from_list"}`. Confirmed absent from a follow-up `list_calendars` call (the still-subscribed "Holidays in Canada" from TC-CAL54 remained, confirming only the targeted subscription was removed).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### TC-CAL57: Unsubscribing does not delete the calendar ⚠️ destructive
|
|
311
|
+
|
|
312
|
+
**Setup:** the calendar removed from the list in TC-CAL56
|
|
313
|
+
|
|
314
|
+
**Prompt**
|
|
315
|
+
> "Subscribe me again to calendar 'en.japanese#holiday@group.v.calendar.google.com'"
|
|
316
|
+
|
|
317
|
+
**Checks**
|
|
318
|
+
- `add_calendar_to_list` succeeds and returns the same `summary` as TC-CAL53
|
|
319
|
+
- Confirms `remove_calendar_from_list` only removed the subscription — the underlying calendar was never affected and is still fully subscribable
|
|
320
|
+
|
|
321
|
+
**Result (2026-07-05) ✅** — Re-subscribed successfully; response had the identical `summary: "Holidays in Japan"` as TC-CAL53. Confirms `remove_calendar_from_list` does not touch the calendar resource itself.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### TC-CAL58: Non-existent calendar ID
|
|
326
|
+
|
|
327
|
+
**Prompt**
|
|
328
|
+
> "Remove calendar 'totally-invalid-cal-id@example.com' from my calendar list"
|
|
329
|
+
|
|
330
|
+
**Checks**
|
|
331
|
+
- Returns `{"error": "..."}` — not a top-level exception
|
|
332
|
+
- No side effects
|
|
333
|
+
|
|
334
|
+
**Result (2026-07-05) ✅** — Returned `{"error": "<HttpError 404 ... 'reason': 'notFound' ...>"}`.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
### TC-CAL59: Cannot unsubscribe from a calendar you own
|
|
339
|
+
|
|
340
|
+
**Setup:** a calendar owned by the authenticated user (e.g. a fresh calendar from `create_calendar`)
|
|
341
|
+
|
|
342
|
+
**Prompt**
|
|
343
|
+
> "Remove calendar {OWNED_CALENDAR_ID} from my calendar list"
|
|
344
|
+
|
|
345
|
+
**Checks**
|
|
346
|
+
- Returns `{"error": "..."}` — not a top-level exception
|
|
347
|
+
- The error message is actionable and names `delete_calendar` as the correct tool — `remove_calendar_from_list` detects the `cannotUnsubscribeFromOwnedCalendar` reason specifically and substitutes a friendly message instead of passing through the raw `HttpError` repr
|
|
348
|
+
|
|
349
|
+
**Result (2026-07-05) ✅** — First pass (before the friendly-message special-case was added) returned the raw passthrough: `{"error": "<HttpError 403 ... 'reason': 'cannotUnsubscribeFromOwnedCalendar', 'message': 'The data owner of a calendar cannot remove such a calendar from their calendar list.' ...>"}`. After adding the special-case (reviewer feedback on #269), re-ran against a freshly created owned calendar and got `{"error": "Google does not allow removing a calendar you own from your own calendar list (reason: cannotUnsubscribeFromOwnedCalendar). Use delete_calendar instead to permanently delete it."}` — confirms the friendlier message is live and correctly names the fix.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
246
353
|
## `list_events`
|
|
247
354
|
|
|
248
355
|
### TC-CAL09: No time filters — upcoming events
|
|
@@ -2015,3 +2015,86 @@ Exporting even the small QA fixture spreadsheet as `xlsx` immediately exceeded t
|
|
|
2015
2015
|
**Background:** `list_file_activity` is already Drive-API-paginated (`page_size` clamped 1–100) and low per-item size — the only realistic exposure is a single activity's `actors` list ballooning on a file with many collaborators. Per the #242 decision doc, this tool intentionally did NOT get a dedicated live-fixture verification (reproducing hundreds of real Drive Activity events isn't cheaply reproducible) — the cap was added for defense-in-depth only, verified by unit tests (`tests/drive/test_activity.py::TestListFileActivity::test_oversized_result_raises`, `test_error_points_to_page_size_not_local_path`).
|
|
2016
2016
|
|
|
2017
2017
|
**Result (2026-07-03) ✅ N/A (by design)** — sanity-checked live that the tool still functions normally post-change (`list_file_activity(file_id={SPREADSHEET_ID}, page_size=5)` returned a normal activity list, no regression). Cap-triggering behavior covered by unit tests only, not live-verified — documented scoping decision, not an oversight.
|
|
2018
|
+
|
|
2019
|
+
---
|
|
2020
|
+
|
|
2021
|
+
## `import_csv_to_sheet` (issue #187)
|
|
2022
|
+
|
|
2023
|
+
### TC-D169: Basic import creates a populated spreadsheet ⚠️ requires-oauth ⚠️ local-filesystem
|
|
2024
|
+
**Prompt**
|
|
2025
|
+
> "Import the CSV at `/tmp/qa-import.csv` into a new spreadsheet called 'QA-CSV-Import' in {FOLDER_ID}" *(create `/tmp/qa-import.csv` first with a header row and 3-4 data rows, e.g. `name,age\nAlice,30\nBob,25`)*
|
|
2026
|
+
|
|
2027
|
+
**Checks**
|
|
2028
|
+
- Response includes `spreadsheetId`, `title` exactly 'QA-CSV-Import', `web_link`, and `rows_written` matching the CSV's row count (including header)
|
|
2029
|
+
- `get_sheet_data` on the new spreadsheet's default sheet returns the same rows, in order
|
|
2030
|
+
- `drive_folder_cache.mark_dirty` fired — `list_files` on {FOLDER_ID} shows the new spreadsheet without a manual refresh
|
|
2031
|
+
|
|
2032
|
+
**Result (2026-07-05) ✅ PASS** Created spreadsheet with `rows_written: 4`, exact title, and a `web_link`. `get_sheet_data` returned `[["name","age"],["Alice","30"],["Bob","25"],["Carol","42"]]` — matches the source CSV exactly. `list_files` on `{FOLDER_ID}` showed the new spreadsheet immediately, confirming the folder cache invalidation.
|
|
2033
|
+
|
|
2034
|
+
---
|
|
2035
|
+
|
|
2036
|
+
### TC-D170: Custom sheet_name renames the default sheet ⚠️ requires-oauth ⚠️ local-filesystem
|
|
2037
|
+
**Prompt**
|
|
2038
|
+
> "Import `/tmp/qa-import.csv` into a new spreadsheet called 'QA-CSV-SheetName', writing to a sheet named 'Imported Data'"
|
|
2039
|
+
|
|
2040
|
+
**Checks**
|
|
2041
|
+
- `list_sheets` on the new spreadsheet shows exactly one sheet, named 'Imported Data' (not the default 'Sheet1')
|
|
2042
|
+
- Data is present on that sheet via `get_sheet_data`
|
|
2043
|
+
|
|
2044
|
+
**Result (2026-07-05) ✅ PASS** `list_sheets` returned exactly `["Imported Data"]` — the default 'Sheet1' was renamed, not left as a second sheet. `get_sheet_data(sheet="Imported Data")` returned all 4 rows intact.
|
|
2045
|
+
|
|
2046
|
+
---
|
|
2047
|
+
|
|
2048
|
+
### TC-D171: Grid auto-expands beyond the default 1000-row limit ⚠️ requires-oauth ⚠️ local-filesystem
|
|
2049
|
+
**Prompt**
|
|
2050
|
+
> "Import `/tmp/qa-import-large.csv` into a new spreadsheet called 'QA-CSV-Large'" *(generate a CSV with 1500+ rows first, e.g. a header plus 1500 numbered rows)*
|
|
2051
|
+
|
|
2052
|
+
**Checks**
|
|
2053
|
+
- Call succeeds without a grid-limit error (the documented pre-fix failure mode from the issue)
|
|
2054
|
+
- `rows_written` matches the CSV's row count
|
|
2055
|
+
- `get_sheet_data` confirms the last row of data is present and readable (not silently truncated at row 1000)
|
|
2056
|
+
|
|
2057
|
+
**Result (2026-07-05) ✅ PASS** Imported a 1501-row CSV (header + 1500 numbered rows) with no grid-limit error — `rows_written: 1501`. Fetched `A999:B1501` and confirmed rows 998–1500 are all present and correctly ordered, including the final row `["1500","row-1500"]` — no truncation at the default 1000-row boundary.
|
|
2058
|
+
|
|
2059
|
+
---
|
|
2060
|
+
|
|
2061
|
+
### TC-D172: Ragged rows padded to a common width ⚠️ requires-oauth ⚠️ local-filesystem
|
|
2062
|
+
**Prompt**
|
|
2063
|
+
> "Import `/tmp/qa-import-ragged.csv` into a new spreadsheet called 'QA-CSV-Ragged'" *(create a CSV where some rows have fewer columns than others, e.g. `a,b,c\n1,2\n`)*
|
|
2064
|
+
|
|
2065
|
+
**Checks**
|
|
2066
|
+
- No dropped or misaligned rows in `get_sheet_data` — short rows are padded with empty cells rather than shifted
|
|
2067
|
+
- `rows_written` counts every row, including the short one
|
|
2068
|
+
|
|
2069
|
+
**Result (2026-07-05) ✅ PASS** CSV `a,b,c / 1,2 / 4,5,6` (middle row missing column `c`) imported with `rows_written: 3`. `get_sheet_data` returned `[["a","b","c"],["1","2"],["4","5","6"]]` — the short row landed under columns a/b with no shift, and the third row wasn't dropped. (Sheets' values.get omits the trailing empty string we pad with, which is expected — the important signal is correct alignment, not a literal empty-string round-trip.)
|
|
2070
|
+
|
|
2071
|
+
---
|
|
2072
|
+
|
|
2073
|
+
### TC-D173: Non-existent local path (unit test)
|
|
2074
|
+
|
|
2075
|
+
**Checks (unit test)**
|
|
2076
|
+
- Missing `local_path` returns `{"error": "..."}` mentioning the path — no Drive API call made
|
|
2077
|
+
|
|
2078
|
+
**Result:** ✅ Unit test `test_file_not_found_returns_error` confirms this — no live Drive call needed since the function returns before touching `ctx`.
|
|
2079
|
+
|
|
2080
|
+
---
|
|
2081
|
+
|
|
2082
|
+
### TC-D174: Non-.csv extension rejected (unit test)
|
|
2083
|
+
|
|
2084
|
+
**Checks (unit test)**
|
|
2085
|
+
- A `.txt` (or other non-.csv) file returns `{"error": "..."}` mentioning `.csv`, without calling the Drive API
|
|
2086
|
+
|
|
2087
|
+
**Result:** ✅ Unit test `test_unsupported_extension_returns_error` confirms this.
|
|
2088
|
+
|
|
2089
|
+
---
|
|
2090
|
+
|
|
2091
|
+
### TC-D175: Service account Drive limitation
|
|
2092
|
+
|
|
2093
|
+
**Prompt**
|
|
2094
|
+
> "Import `/tmp/qa-import.csv` into a new spreadsheet called 'QA-CSV-SA-Limit' — I want to verify whether the service account can create in personal Drive"
|
|
2095
|
+
|
|
2096
|
+
**Checks**
|
|
2097
|
+
- 🔍 **Known limitation:** same as `create_spreadsheet` (TC-D04) — service account cannot create in personal Drive, only shared folders it has access to
|
|
2098
|
+
- Unit-level equivalent already covered: `test_storage_quota_error_returns_helpful_message`
|
|
2099
|
+
|
|
2100
|
+
**Result (2026-07-05) ✅ PASS** Against the service-account server (`mcp-gee-sweet-sa`), the call returned `{"error": "Service accounts cannot create or copy files in personal Drive (no storage quota). Use OAuth or ADC auth for full Drive write access, or use a Shared Drive destination. Check server://auth-status for your current auth method and affected tools."}` — the same `_SA_QUOTA_ERROR` message `create_spreadsheet` returns, confirming the shared error path works for this tool too.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Auto-generated by `scripts/gen_tool_docs.py` — do not edit by hand, your changes will be overwritten on the next commit. -->
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
90 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
|
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
| Tool | Description | Key parameters |
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `create_spreadsheet` | Create a new Google Spreadsheet. | `title`, `folder_id?` |
|
|
50
|
+
| `import_csv_to_sheet` | Create a new Google Spreadsheet populated from a local CSV file. | `local_path`, `title`, `folder_id?`, `sheet_name?` |
|
|
50
51
|
| `list_spreadsheets` | List all spreadsheets in the specified Google Drive folder. If no folder is specified, uses the configured default folder or lists from 'My Drive'. | `folder_id?` |
|
|
51
52
|
| `search_spreadsheets` | Search for spreadsheets in Google Drive by name or content. | `query`, `max_results?` |
|
|
52
53
|
|
|
@@ -144,6 +145,8 @@ Requires the `drive.activity.readonly` scope; uses the Drive Activity API v2.
|
|
|
144
145
|
| `create_calendar` | Create a new secondary calendar owned by the authenticated user. | `summary`, `description?`, `timezone?` |
|
|
145
146
|
| `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
147
|
| `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` |
|
|
148
|
+
| `add_calendar_to_list` | Subscribe the authenticated user (or service account) to a calendar, adding it to their calendar list so it appears in list_calendars. | `calendar_id`, `color_id?` |
|
|
149
|
+
| `remove_calendar_from_list` | Unsubscribe from a calendar, removing it from the authenticated user's calendar list. The calendar itself is not deleted and is unaffected for any other user it's shared with — use delete_calendar to permanently delete a calendar you own. | `calendar_id` |
|
|
147
150
|
| `list_events` | List events in a calendar. | `calendar_id`, `time_min?`, `time_max?`, `query?`, `max_results?`, `expand_recurring?` |
|
|
148
151
|
| `get_event` | Fetch a single event by calendar ID and event ID. | `calendar_id`, `event_id` |
|
|
149
152
|
| `create_event` | Create a new event in a calendar. | `calendar_id`, `summary`, `start`, `end`, `description?`, `location?`, `attendees?`, `timezone?`, `recurrence?` |
|
|
@@ -97,7 +97,13 @@ SECTIONS = [
|
|
|
97
97
|
"Spreadsheets (Drive-level)",
|
|
98
98
|
lambda f: (
|
|
99
99
|
f.__module__ == "mcp_gee_sweet.tools.drive.files"
|
|
100
|
-
and f.__name__
|
|
100
|
+
and f.__name__
|
|
101
|
+
in {
|
|
102
|
+
"create_spreadsheet",
|
|
103
|
+
"list_spreadsheets",
|
|
104
|
+
"search_spreadsheets",
|
|
105
|
+
"import_csv_to_sheet",
|
|
106
|
+
}
|
|
101
107
|
),
|
|
102
108
|
"`create_spreadsheet` cannot create files in a personal Drive when using "
|
|
103
109
|
"service account auth — use OAuth or a Shared Drive.",
|
|
@@ -106,7 +112,13 @@ SECTIONS = [
|
|
|
106
112
|
"Drive — files",
|
|
107
113
|
lambda f: (
|
|
108
114
|
f.__module__ == "mcp_gee_sweet.tools.drive.files"
|
|
109
|
-
and f.__name__
|
|
115
|
+
and f.__name__
|
|
116
|
+
not in {
|
|
117
|
+
"create_spreadsheet",
|
|
118
|
+
"list_spreadsheets",
|
|
119
|
+
"search_spreadsheets",
|
|
120
|
+
"import_csv_to_sheet",
|
|
121
|
+
}
|
|
110
122
|
),
|
|
111
123
|
None,
|
|
112
124
|
),
|
|
@@ -194,6 +206,7 @@ SUBSETS = [
|
|
|
194
206
|
"rename_sheet",
|
|
195
207
|
"add_rows",
|
|
196
208
|
"add_columns",
|
|
209
|
+
"import_csv_to_sheet",
|
|
197
210
|
],
|
|
198
211
|
),
|
|
199
212
|
(
|
|
@@ -218,6 +231,8 @@ SUBSETS = [
|
|
|
218
231
|
"create_calendar",
|
|
219
232
|
"update_calendar",
|
|
220
233
|
"delete_calendar",
|
|
234
|
+
"add_calendar_to_list",
|
|
235
|
+
"remove_calendar_from_list",
|
|
221
236
|
"list_events",
|
|
222
237
|
"get_event",
|
|
223
238
|
"create_event",
|
|
@@ -2,11 +2,18 @@ import logging
|
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
+
from googleapiclient.errors import HttpError
|
|
5
6
|
from mcp.server.fastmcp import Context
|
|
6
7
|
from mcp.types import ToolAnnotations
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
11
|
+
_CANNOT_UNSUBSCRIBE_OWNED_ERROR = (
|
|
12
|
+
"Google does not allow removing a calendar you own from your own calendar list "
|
|
13
|
+
"(reason: cannotUnsubscribeFromOwnedCalendar). Use delete_calendar instead to "
|
|
14
|
+
"permanently delete it."
|
|
15
|
+
)
|
|
16
|
+
|
|
10
17
|
|
|
11
18
|
def register(tool):
|
|
12
19
|
@tool(annotations=ToolAnnotations(title="List Calendars", readOnlyHint=True))
|
|
@@ -207,6 +214,81 @@ def register(tool):
|
|
|
207
214
|
logger.debug("Deleted calendar %s", calendar_id)
|
|
208
215
|
return {"calendar_id": calendar_id, "action": "deleted"}
|
|
209
216
|
|
|
217
|
+
@tool(annotations=ToolAnnotations(title="Add Calendar To List", destructiveHint=True))
|
|
218
|
+
def add_calendar_to_list(
|
|
219
|
+
calendar_id: str,
|
|
220
|
+
color_id: str | None = None,
|
|
221
|
+
ctx: Context = None,
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
"""
|
|
224
|
+
Subscribe the authenticated user (or service account) to a calendar,
|
|
225
|
+
adding it to their calendar list so it appears in list_calendars.
|
|
226
|
+
|
|
227
|
+
This does not create or share a calendar — it only adds an existing,
|
|
228
|
+
already-accessible calendar to your own list. For service accounts,
|
|
229
|
+
this is the required step before a calendar shared with the service
|
|
230
|
+
account's email will show up in list_calendars.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
calendar_id: The calendar ID to subscribe to (e.g. the owner's email
|
|
234
|
+
for a personal calendar, or a `...@group.calendar.google.com` ID).
|
|
235
|
+
color_id: Optional color ID from the palette returned by the Calendar
|
|
236
|
+
API's colors().get() endpoint (predefined IDs, typically '1'-'24').
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The new calendar list entry: id, summary, time_zone, access_role,
|
|
240
|
+
primary, color_id.
|
|
241
|
+
"""
|
|
242
|
+
lc = ctx.request_context.lifespan_context
|
|
243
|
+
|
|
244
|
+
body: dict[str, Any] = {"id": calendar_id}
|
|
245
|
+
if color_id is not None:
|
|
246
|
+
body["colorId"] = color_id
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
c = lc.calendar_service.calendarList().insert(body=body).execute()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return {"error": str(e)}
|
|
252
|
+
|
|
253
|
+
lc.calendar_cache.mark_dirty(c["id"])
|
|
254
|
+
logger.debug("Added calendar %s to calendar list", c["id"])
|
|
255
|
+
return {
|
|
256
|
+
"id": c["id"],
|
|
257
|
+
"summary": c.get("summary", ""),
|
|
258
|
+
"time_zone": c.get("timeZone"),
|
|
259
|
+
"access_role": c.get("accessRole"),
|
|
260
|
+
"primary": c.get("primary", False),
|
|
261
|
+
"color_id": c.get("colorId"),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@tool(annotations=ToolAnnotations(title="Remove Calendar From List", destructiveHint=True))
|
|
265
|
+
def remove_calendar_from_list(calendar_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
266
|
+
"""
|
|
267
|
+
Unsubscribe from a calendar, removing it from the authenticated user's
|
|
268
|
+
calendar list. The calendar itself is not deleted and is unaffected for
|
|
269
|
+
any other user it's shared with — use delete_calendar to permanently
|
|
270
|
+
delete a calendar you own.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
calendar_id: The calendar ID to remove from the list.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Confirmation with calendar_id and action 'removed_from_list'.
|
|
277
|
+
"""
|
|
278
|
+
lc = ctx.request_context.lifespan_context
|
|
279
|
+
try:
|
|
280
|
+
lc.calendar_service.calendarList().delete(calendarId=calendar_id).execute()
|
|
281
|
+
except HttpError as e:
|
|
282
|
+
if e.resp.status == 403 and b"cannotUnsubscribeFromOwnedCalendar" in (e.content or b""):
|
|
283
|
+
return {"error": _CANNOT_UNSUBSCRIBE_OWNED_ERROR}
|
|
284
|
+
return {"error": str(e)}
|
|
285
|
+
except Exception as e:
|
|
286
|
+
return {"error": str(e)}
|
|
287
|
+
|
|
288
|
+
lc.calendar_cache.mark_dirty(calendar_id)
|
|
289
|
+
logger.debug("Removed calendar %s from calendar list", calendar_id)
|
|
290
|
+
return {"calendar_id": calendar_id, "action": "removed_from_list"}
|
|
291
|
+
|
|
210
292
|
@tool(annotations=ToolAnnotations(title="List Events", readOnlyHint=True))
|
|
211
293
|
def list_events(
|
|
212
294
|
calendar_id: str,
|
{mcp_gee_sweet-0.7.0.dev69 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/files.py
RENAMED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import csv
|
|
1
2
|
import logging
|
|
2
3
|
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
5
7
|
from googleapiclient.errors import HttpError
|
|
6
8
|
from mcp.server.fastmcp import Context
|
|
7
9
|
from mcp.types import ToolAnnotations
|
|
8
10
|
|
|
11
|
+
from ..sheets.helpers import _quote_sheet_name
|
|
9
12
|
from . import _SA_QUOTA_ERROR
|
|
10
13
|
|
|
11
14
|
logger = logging.getLogger(__name__)
|
|
12
15
|
|
|
16
|
+
_CSV_IMPORT_CHUNK_ROWS = 5000
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
def register(tool):
|
|
15
20
|
@tool(annotations=ToolAnnotations(title="Create Spreadsheet", destructiveHint=True))
|
|
@@ -71,6 +76,155 @@ def register(tool):
|
|
|
71
76
|
"folder": parents[0] if parents else "root",
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
@tool(annotations=ToolAnnotations(title="Import CSV to Sheet", destructiveHint=True))
|
|
80
|
+
def import_csv_to_sheet(
|
|
81
|
+
local_path: str,
|
|
82
|
+
title: str,
|
|
83
|
+
folder_id: str | None = None,
|
|
84
|
+
sheet_name: str = "Sheet1",
|
|
85
|
+
ctx: Context = None,
|
|
86
|
+
) -> dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Create a new Google Spreadsheet populated from a local CSV file.
|
|
89
|
+
|
|
90
|
+
Reads the CSV, creates the spreadsheet, expands the sheet's grid to fit
|
|
91
|
+
the data (avoiding the default 1000-row/26-column limit), and writes
|
|
92
|
+
every row in one or more batched value updates.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
local_path: Absolute path to the local .csv file.
|
|
96
|
+
title: Title for the new spreadsheet.
|
|
97
|
+
folder_id: Optional Google Drive folder ID where the spreadsheet should
|
|
98
|
+
be created. If not provided, uses the configured default
|
|
99
|
+
folder or creates in root.
|
|
100
|
+
sheet_name: Name of the sheet the data is written to (default "Sheet1").
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
spreadsheetId, title, web_link, and rows_written.
|
|
104
|
+
|
|
105
|
+
Note:
|
|
106
|
+
Requires OAuth or ADC auth. Service accounts cannot create files in personal
|
|
107
|
+
Drive (no storage quota). Works on Shared Drives regardless of auth method.
|
|
108
|
+
Check server://auth-status for your current auth method.
|
|
109
|
+
"""
|
|
110
|
+
path = Path(local_path)
|
|
111
|
+
if not path.exists():
|
|
112
|
+
return {"error": f"File not found: {local_path}"}
|
|
113
|
+
if path.suffix.lower() != ".csv":
|
|
114
|
+
return {"error": f"Unsupported file extension '{path.suffix}'. Use .csv"}
|
|
115
|
+
|
|
116
|
+
with path.open(newline="", encoding="utf-8") as f:
|
|
117
|
+
rows = list(csv.reader(f))
|
|
118
|
+
|
|
119
|
+
if not rows:
|
|
120
|
+
return {"error": f"CSV file is empty: {local_path}"}
|
|
121
|
+
|
|
122
|
+
width = max(len(row) for row in rows)
|
|
123
|
+
rows = [row + [""] * (width - len(row)) for row in rows]
|
|
124
|
+
|
|
125
|
+
lc = ctx.request_context.lifespan_context
|
|
126
|
+
drive_service = lc.drive_service
|
|
127
|
+
sheets_service = lc.sheets_service
|
|
128
|
+
target_folder_id = folder_id or lc.folder_id
|
|
129
|
+
|
|
130
|
+
file_body = {"name": title, "mimeType": "application/vnd.google-apps.spreadsheet"}
|
|
131
|
+
if target_folder_id:
|
|
132
|
+
file_body["parents"] = [target_folder_id]
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
spreadsheet = (
|
|
136
|
+
drive_service.files()
|
|
137
|
+
.create(
|
|
138
|
+
supportsAllDrives=True,
|
|
139
|
+
body=file_body,
|
|
140
|
+
fields="id, name, parents, webViewLink",
|
|
141
|
+
)
|
|
142
|
+
.execute()
|
|
143
|
+
)
|
|
144
|
+
except HttpError as e:
|
|
145
|
+
if e.resp.status == 403 and b"storageQuotaExceeded" in (e.content or b""):
|
|
146
|
+
return {"error": _SA_QUOTA_ERROR}
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
spreadsheet_id = spreadsheet.get("id")
|
|
150
|
+
target_folder_id = target_folder_id or (spreadsheet.get("parents", [None])[0])
|
|
151
|
+
|
|
152
|
+
default_sheet = (
|
|
153
|
+
sheets_service.spreadsheets()
|
|
154
|
+
.get(
|
|
155
|
+
spreadsheetId=spreadsheet_id,
|
|
156
|
+
fields="sheets(properties(sheetId,title,gridProperties))",
|
|
157
|
+
)
|
|
158
|
+
.execute()
|
|
159
|
+
.get("sheets", [{}])[0]
|
|
160
|
+
)
|
|
161
|
+
default_props = default_sheet.get("properties", {})
|
|
162
|
+
sheet_id = default_props.get("sheetId", 0)
|
|
163
|
+
default_title = default_props.get("title")
|
|
164
|
+
grid = default_props.get("gridProperties", {})
|
|
165
|
+
|
|
166
|
+
requests: list[dict[str, Any]] = []
|
|
167
|
+
if default_title != sheet_name:
|
|
168
|
+
requests.append(
|
|
169
|
+
{
|
|
170
|
+
"updateSheetProperties": {
|
|
171
|
+
"properties": {"sheetId": sheet_id, "title": sheet_name},
|
|
172
|
+
"fields": "title",
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
needed_rows, needed_cols = len(rows), width
|
|
177
|
+
if needed_rows > grid.get("rowCount", 1000) or needed_cols > grid.get("columnCount", 26):
|
|
178
|
+
requests.append(
|
|
179
|
+
{
|
|
180
|
+
"updateSheetProperties": {
|
|
181
|
+
"properties": {
|
|
182
|
+
"sheetId": sheet_id,
|
|
183
|
+
"gridProperties": {
|
|
184
|
+
"rowCount": max(needed_rows, grid.get("rowCount", 1000)),
|
|
185
|
+
"columnCount": max(needed_cols, grid.get("columnCount", 26)),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
"fields": "gridProperties.rowCount,gridProperties.columnCount",
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if requests:
|
|
194
|
+
sheets_service.spreadsheets().batchUpdate(
|
|
195
|
+
spreadsheetId=spreadsheet_id, body={"requests": requests}
|
|
196
|
+
).execute()
|
|
197
|
+
lc.cache.mark_dirty(spreadsheet_id)
|
|
198
|
+
|
|
199
|
+
quoted_sheet = _quote_sheet_name(sheet_name)
|
|
200
|
+
for start in range(0, len(rows), _CSV_IMPORT_CHUNK_ROWS):
|
|
201
|
+
chunk = rows[start : start + _CSV_IMPORT_CHUNK_ROWS]
|
|
202
|
+
sheets_service.spreadsheets().values().update(
|
|
203
|
+
spreadsheetId=spreadsheet_id,
|
|
204
|
+
range=f"{quoted_sheet}!A{start + 1}",
|
|
205
|
+
valueInputOption="USER_ENTERED",
|
|
206
|
+
body={"values": chunk},
|
|
207
|
+
).execute()
|
|
208
|
+
|
|
209
|
+
if target_folder_id:
|
|
210
|
+
lc.drive_folder_cache.mark_dirty(target_folder_id)
|
|
211
|
+
lc.sheet_data_cache.mark_dirty(spreadsheet_id)
|
|
212
|
+
|
|
213
|
+
logger.debug(
|
|
214
|
+
"Imported CSV %s into spreadsheet %s (%d rows, %d cols)",
|
|
215
|
+
local_path,
|
|
216
|
+
spreadsheet_id,
|
|
217
|
+
len(rows),
|
|
218
|
+
width,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"spreadsheetId": spreadsheet_id,
|
|
223
|
+
"title": spreadsheet.get("name", title),
|
|
224
|
+
"web_link": spreadsheet.get("webViewLink"),
|
|
225
|
+
"rows_written": len(rows),
|
|
226
|
+
}
|
|
227
|
+
|
|
74
228
|
@tool(annotations=ToolAnnotations(title="List Spreadsheets", readOnlyHint=True))
|
|
75
229
|
def list_spreadsheets(
|
|
76
230
|
folder_id: str | None = None, ctx: Context = None
|