mcp-gee-sweet 0.7.0.dev70__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.dev70 → mcp_gee_sweet-0.7.0.dev72}/CLAUDE.md +1 -1
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/configuration.md +2 -2
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/drive.md +83 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/tools.md +2 -1
- mcp_gee_sweet-0.7.0.dev72/pyrightconfig.json +4 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/scripts/gen_tool_docs.py +15 -2
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/files.py +154 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_files.py +224 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.dockerignore +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.gitignore +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.python-version +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/Dockerfile +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/LICENSE +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/Makefile +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → 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.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/roadmap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev70 → 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:**
|
|
@@ -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
|
|
|
@@ -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
|
(
|
{mcp_gee_sweet-0.7.0.dev70 → 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
|
|
@@ -344,3 +344,227 @@ class TestGetStorageQuota:
|
|
|
344
344
|
fields_arg = svc.about.return_value.get.call_args.kwargs["fields"]
|
|
345
345
|
assert "storageQuota" in fields_arg
|
|
346
346
|
assert "user" in fields_arg
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class TestImportCsvToSheet:
|
|
350
|
+
"""import_csv_to_sheet: create a spreadsheet from a local CSV file."""
|
|
351
|
+
|
|
352
|
+
def _drive_service(self):
|
|
353
|
+
mock = MagicMock()
|
|
354
|
+
mock.files.return_value.create.return_value.execute.return_value = {
|
|
355
|
+
"id": "sheet123",
|
|
356
|
+
"name": "Imported",
|
|
357
|
+
"parents": ["folder1"],
|
|
358
|
+
"webViewLink": "https://docs.google.com/spreadsheets/d/sheet123",
|
|
359
|
+
}
|
|
360
|
+
return mock
|
|
361
|
+
|
|
362
|
+
def _sheets_service(self, sheet_title="Sheet1", row_count=1000, column_count=26):
|
|
363
|
+
mock = MagicMock()
|
|
364
|
+
mock.spreadsheets.return_value.get.return_value.execute.return_value = {
|
|
365
|
+
"sheets": [
|
|
366
|
+
{
|
|
367
|
+
"properties": {
|
|
368
|
+
"sheetId": 0,
|
|
369
|
+
"title": sheet_title,
|
|
370
|
+
"gridProperties": {
|
|
371
|
+
"rowCount": row_count,
|
|
372
|
+
"columnCount": column_count,
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
mock.spreadsheets.return_value.batchUpdate.return_value.execute.return_value = {}
|
|
379
|
+
mock.spreadsheets.return_value.values.return_value.update.return_value.execute.return_value = {}
|
|
380
|
+
return mock
|
|
381
|
+
|
|
382
|
+
def _write_csv(self, tmp_path, rows, name="data.csv"):
|
|
383
|
+
path = tmp_path / name
|
|
384
|
+
path.write_text("\n".join(",".join(row) for row in rows) + "\n", encoding="utf-8")
|
|
385
|
+
return path
|
|
386
|
+
|
|
387
|
+
def test_file_not_found_returns_error(self):
|
|
388
|
+
ctx = _make_ctx()
|
|
389
|
+
result = _drive_tools["import_csv_to_sheet"](
|
|
390
|
+
local_path="/no/such/file.csv", title="X", ctx=ctx
|
|
391
|
+
)
|
|
392
|
+
assert "error" in result
|
|
393
|
+
assert "not found" in result["error"].lower()
|
|
394
|
+
|
|
395
|
+
def test_unsupported_extension_returns_error(self, tmp_path):
|
|
396
|
+
path = tmp_path / "data.txt"
|
|
397
|
+
path.write_text("a,b\n1,2\n", encoding="utf-8")
|
|
398
|
+
ctx = _make_ctx()
|
|
399
|
+
result = _drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
400
|
+
assert "error" in result
|
|
401
|
+
assert ".csv" in result["error"]
|
|
402
|
+
|
|
403
|
+
def test_empty_csv_returns_error(self, tmp_path):
|
|
404
|
+
path = tmp_path / "empty.csv"
|
|
405
|
+
path.write_text("", encoding="utf-8")
|
|
406
|
+
ctx = _make_ctx()
|
|
407
|
+
result = _drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
408
|
+
assert "error" in result
|
|
409
|
+
assert "empty" in result["error"].lower()
|
|
410
|
+
|
|
411
|
+
def test_creates_spreadsheet_and_writes_rows(self, tmp_path):
|
|
412
|
+
path = self._write_csv(tmp_path, [["name", "age"], ["Alice", "30"], ["Bob", "25"]])
|
|
413
|
+
drive_svc = self._drive_service()
|
|
414
|
+
sheets_svc = self._sheets_service()
|
|
415
|
+
folder_cache = MagicMock()
|
|
416
|
+
sheet_data_cache = MagicMock()
|
|
417
|
+
ctx = _make_ctx(
|
|
418
|
+
drive_service=drive_svc,
|
|
419
|
+
sheets_service=sheets_svc,
|
|
420
|
+
drive_folder_cache=folder_cache,
|
|
421
|
+
sheet_data_cache=sheet_data_cache,
|
|
422
|
+
folder_id=None,
|
|
423
|
+
)
|
|
424
|
+
result = _drive_tools["import_csv_to_sheet"](
|
|
425
|
+
local_path=str(path), title="Imported", folder_id="folder1", ctx=ctx
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
assert result["spreadsheetId"] == "sheet123"
|
|
429
|
+
assert result["title"] == "Imported"
|
|
430
|
+
assert result["web_link"] == "https://docs.google.com/spreadsheets/d/sheet123"
|
|
431
|
+
assert result["rows_written"] == 3
|
|
432
|
+
|
|
433
|
+
update_call = sheets_svc.spreadsheets.return_value.values.return_value.update
|
|
434
|
+
assert update_call.call_count == 1
|
|
435
|
+
kwargs = update_call.call_args.kwargs
|
|
436
|
+
assert kwargs["spreadsheetId"] == "sheet123"
|
|
437
|
+
assert kwargs["range"] == "Sheet1!A1"
|
|
438
|
+
assert kwargs["valueInputOption"] == "USER_ENTERED"
|
|
439
|
+
assert kwargs["body"]["values"] == [
|
|
440
|
+
["name", "age"],
|
|
441
|
+
["Alice", "30"],
|
|
442
|
+
["Bob", "25"],
|
|
443
|
+
]
|
|
444
|
+
folder_cache.mark_dirty.assert_called_once_with("folder1")
|
|
445
|
+
sheet_data_cache.mark_dirty.assert_called_once_with("sheet123")
|
|
446
|
+
# No resize/rename needed — default title matches, data fits default grid.
|
|
447
|
+
sheets_svc.spreadsheets.return_value.batchUpdate.assert_not_called()
|
|
448
|
+
|
|
449
|
+
def test_pads_ragged_rows_to_common_width(self, tmp_path):
|
|
450
|
+
path = tmp_path / "ragged.csv"
|
|
451
|
+
path.write_text("a,b,c\n1,2\n", encoding="utf-8")
|
|
452
|
+
drive_svc = self._drive_service()
|
|
453
|
+
sheets_svc = self._sheets_service()
|
|
454
|
+
ctx = _make_ctx(
|
|
455
|
+
drive_service=drive_svc,
|
|
456
|
+
sheets_service=sheets_svc,
|
|
457
|
+
drive_folder_cache=MagicMock(),
|
|
458
|
+
sheet_data_cache=MagicMock(),
|
|
459
|
+
folder_id=None,
|
|
460
|
+
)
|
|
461
|
+
_drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
462
|
+
kwargs = sheets_svc.spreadsheets.return_value.values.return_value.update.call_args.kwargs
|
|
463
|
+
assert kwargs["body"]["values"] == [["a", "b", "c"], ["1", "2", ""]]
|
|
464
|
+
|
|
465
|
+
def test_renames_default_sheet_when_sheet_name_differs(self, tmp_path):
|
|
466
|
+
path = self._write_csv(tmp_path, [["a"], ["1"]])
|
|
467
|
+
drive_svc = self._drive_service()
|
|
468
|
+
sheets_svc = self._sheets_service(sheet_title="Sheet1")
|
|
469
|
+
ctx = _make_ctx(
|
|
470
|
+
drive_service=drive_svc,
|
|
471
|
+
sheets_service=sheets_svc,
|
|
472
|
+
drive_folder_cache=MagicMock(),
|
|
473
|
+
sheet_data_cache=MagicMock(),
|
|
474
|
+
cache=MagicMock(),
|
|
475
|
+
folder_id=None,
|
|
476
|
+
)
|
|
477
|
+
_drive_tools["import_csv_to_sheet"](
|
|
478
|
+
local_path=str(path), title="X", sheet_name="Imported Data", ctx=ctx
|
|
479
|
+
)
|
|
480
|
+
requests = sheets_svc.spreadsheets.return_value.batchUpdate.call_args.kwargs["body"][
|
|
481
|
+
"requests"
|
|
482
|
+
]
|
|
483
|
+
assert requests[0]["updateSheetProperties"]["properties"]["title"] == "Imported Data"
|
|
484
|
+
assert requests[0]["updateSheetProperties"]["properties"]["sheetId"] == 0
|
|
485
|
+
assert requests[0]["updateSheetProperties"]["fields"] == "title"
|
|
486
|
+
update_range = (
|
|
487
|
+
sheets_svc.spreadsheets.return_value.values.return_value.update.call_args.kwargs[
|
|
488
|
+
"range"
|
|
489
|
+
]
|
|
490
|
+
)
|
|
491
|
+
assert update_range == "'Imported Data'!A1"
|
|
492
|
+
|
|
493
|
+
def test_resizes_grid_when_data_exceeds_default(self, tmp_path):
|
|
494
|
+
path = self._write_csv(tmp_path, [["a"], ["1"], ["2"], ["3"]])
|
|
495
|
+
drive_svc = self._drive_service()
|
|
496
|
+
# Force a resize by mocking a grid smaller than our 4-row CSV.
|
|
497
|
+
sheets_svc = self._sheets_service(row_count=1, column_count=1)
|
|
498
|
+
cache = MagicMock()
|
|
499
|
+
ctx = _make_ctx(
|
|
500
|
+
drive_service=drive_svc,
|
|
501
|
+
sheets_service=sheets_svc,
|
|
502
|
+
drive_folder_cache=MagicMock(),
|
|
503
|
+
sheet_data_cache=MagicMock(),
|
|
504
|
+
cache=cache,
|
|
505
|
+
folder_id=None,
|
|
506
|
+
)
|
|
507
|
+
_drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
508
|
+
requests = sheets_svc.spreadsheets.return_value.batchUpdate.call_args.kwargs["body"][
|
|
509
|
+
"requests"
|
|
510
|
+
]
|
|
511
|
+
resize_req = next(
|
|
512
|
+
r for r in requests if "gridProperties" in r["updateSheetProperties"]["properties"]
|
|
513
|
+
)
|
|
514
|
+
grid = resize_req["updateSheetProperties"]["properties"]["gridProperties"]
|
|
515
|
+
assert grid["rowCount"] == 4
|
|
516
|
+
assert grid["columnCount"] == 1
|
|
517
|
+
cache.mark_dirty.assert_called_once_with("sheet123")
|
|
518
|
+
|
|
519
|
+
def test_no_resize_when_data_fits_default_grid(self, tmp_path):
|
|
520
|
+
path = self._write_csv(tmp_path, [["a", "b"], ["1", "2"]])
|
|
521
|
+
drive_svc = self._drive_service()
|
|
522
|
+
sheets_svc = self._sheets_service(row_count=1000, column_count=26)
|
|
523
|
+
cache = MagicMock()
|
|
524
|
+
ctx = _make_ctx(
|
|
525
|
+
drive_service=drive_svc,
|
|
526
|
+
sheets_service=sheets_svc,
|
|
527
|
+
drive_folder_cache=MagicMock(),
|
|
528
|
+
sheet_data_cache=MagicMock(),
|
|
529
|
+
cache=cache,
|
|
530
|
+
folder_id=None,
|
|
531
|
+
)
|
|
532
|
+
_drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
533
|
+
sheets_svc.spreadsheets.return_value.batchUpdate.assert_not_called()
|
|
534
|
+
cache.mark_dirty.assert_not_called()
|
|
535
|
+
|
|
536
|
+
def test_chunks_large_row_counts(self, tmp_path, monkeypatch):
|
|
537
|
+
monkeypatch.setattr(drive_files_module, "_CSV_IMPORT_CHUNK_ROWS", 2)
|
|
538
|
+
rows = [["a"]] + [[str(i)] for i in range(5)]
|
|
539
|
+
path = self._write_csv(tmp_path, rows)
|
|
540
|
+
drive_svc = self._drive_service()
|
|
541
|
+
sheets_svc = self._sheets_service()
|
|
542
|
+
ctx = _make_ctx(
|
|
543
|
+
drive_service=drive_svc,
|
|
544
|
+
sheets_service=sheets_svc,
|
|
545
|
+
drive_folder_cache=MagicMock(),
|
|
546
|
+
sheet_data_cache=MagicMock(),
|
|
547
|
+
folder_id=None,
|
|
548
|
+
)
|
|
549
|
+
result = _drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
550
|
+
assert result["rows_written"] == 6
|
|
551
|
+
update_call = sheets_svc.spreadsheets.return_value.values.return_value.update
|
|
552
|
+
assert update_call.call_count == 3
|
|
553
|
+
ranges = [c.kwargs["range"] for c in update_call.call_args_list]
|
|
554
|
+
assert ranges == ["Sheet1!A1", "Sheet1!A3", "Sheet1!A5"]
|
|
555
|
+
|
|
556
|
+
def test_storage_quota_error_returns_helpful_message(self, tmp_path):
|
|
557
|
+
path = self._write_csv(tmp_path, [["a"], ["1"]])
|
|
558
|
+
drive_svc = MagicMock()
|
|
559
|
+
drive_svc.files.return_value.create.return_value.execute.side_effect = _quota_http_error()
|
|
560
|
+
ctx = _make_ctx(drive_service=drive_svc, sheets_service=MagicMock(), folder_id=None)
|
|
561
|
+
result = _drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
562
|
+
assert result["error"] == drive_files_module._SA_QUOTA_ERROR
|
|
563
|
+
|
|
564
|
+
def test_other_403_error_reraises(self, tmp_path):
|
|
565
|
+
path = self._write_csv(tmp_path, [["a"], ["1"]])
|
|
566
|
+
drive_svc = MagicMock()
|
|
567
|
+
drive_svc.files.return_value.create.return_value.execute.side_effect = _other_403_error()
|
|
568
|
+
ctx = _make_ctx(drive_service=drive_svc, sheets_service=MagicMock(), folder_id=None)
|
|
569
|
+
with pytest.raises(HttpError):
|
|
570
|
+
_drive_tools["import_csv_to_sheet"](local_path=str(path), title="X", ctx=ctx)
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/ISSUE_TEMPLATE/bug_report.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-chart-theming.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-composite-tools.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-docs-formatting.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/decisions/decision-publishing.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/design/effectiveformat-spike.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d195-create-doc.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d196-create-doc.html
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d213-dollar-escape.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/fixtures/tc-d226-heading-table.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-drive-smoke.md
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/docs/qa/results/2026-07-04-sheets_read.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/content.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/emitter.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/html_parser.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/layout.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/style.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/docs/tables.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/__init__.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/activity.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/sharing.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/drive/transfer.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/response_limits.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/__init__.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/data.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/helpers.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev70 → mcp_gee_sweet-0.7.0.dev72}/src/mcp_gee_sweet/tools/sheets/structure.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|