mcp-gee-sweet 0.7.0.dev84__tar.gz → 0.7.0.dev87__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.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/merge-pr.md +3 -2
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/verify-pr.md +16 -2
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/CLAUDE.md +1 -1
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/configuration.md +1 -1
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/sheets_mgmt.md +88 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/roadmap.md +1 -1
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/tools.md +2 -1
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/helpers.py +16 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/structure.py +61 -1
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/sheets/test_structure.py +152 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.dockerignore +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.gitignore +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.python-version +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/Dockerfile +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/LICENSE +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/Makefile +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/design.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/drive.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/uv.lock +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Merge the current branch's PR and clean up its worktree. Uses `--admin` only to bypass the required-review gate (this is a solo-maintainer repo, so a second approver is never coming) — it must never be used to bypass a failing or pending status check.
|
|
2
2
|
|
|
3
|
-
1. Get the PR for the current branch: `gh pr view --json number,title,url,state,mergeable,statusCheckRollup,reviewDecision`. If there's no open PR for this branch, stop and tell the user.
|
|
3
|
+
1. Get the PR: if given a PR number (e.g. following on from `/verify-pr <number>`), use `gh pr view <number> --json ...` explicitly rather than relying on current-branch detection — `/verify-pr` may have checked out a locally-named `review/<headRefName>` branch (see its step 3) instead of the PR's real branch name, which current-branch auto-detection can't always resolve. Otherwise, get the PR for the current branch: `gh pr view --json number,title,url,state,mergeable,statusCheckRollup,reviewDecision`. If there's no open PR for this branch/number, stop and tell the user.
|
|
4
4
|
2. Verify it's actually safe to merge:
|
|
5
5
|
- `state` must be `OPEN`.
|
|
6
6
|
- Every check in `statusCheckRollup` (in particular "Lint and test") must show success. If anything is failing or still pending, stop and report which — do not merge, and do not wait/poll.
|
|
7
7
|
- `mergeable` must not be `CONFLICTING`.
|
|
8
8
|
3. Show the user a summary: PR number, title, URL, check status, and review status (expected to be blocked only on the missing-approval requirement). State plainly that merging will use `--admin` to bypass that review gate. Ask for explicit confirmation before proceeding.
|
|
9
9
|
4. On confirmation, squash-merge (matching this repo's existing history — commits land on `develop` as a single `title (#number)` commit): `gh pr merge <number> --admin --squash`. The remote branch is auto-deleted on merge (repo setting), so don't pass `--delete-branch`.
|
|
10
|
-
5. Clean up
|
|
10
|
+
5. Clean up:
|
|
11
|
+
- If the current branch is a `review/<headRefName>` branch created by `/verify-pr` (not a `.claude/worktrees/*` worktree — just a local branch in the main checkout), there's no worktree to remove: switch to `develop` and `git branch -D review/<headRefName>`.
|
|
11
12
|
- If this session created the current worktree (i.e. it got here via `/start-worktree` or `/next-issue` earlier in this same session), use `ExitWorktree` with `action: "remove"`.
|
|
12
13
|
- Otherwise (worktree predates this session), fall back to manual cleanup from the main checkout: `git worktree remove <path>`, then `git branch -d <branch>`, then `git worktree prune`.
|
|
13
14
|
6. Update the roadmap: from the main checkout on `develop`, `git pull` then check `docs/roadmap.md` for a checkbox line referencing the issue(s) this PR closes (search by issue number). If found, check it off (`- [ ]` → `- [x]`) and append `(PR <number>)` in the existing style (e.g. `(PR #280)`) — see recent `- [x]` entries for the exact convention. If the roadmap has no line item for this issue (not every ticket is roadmap-tracked — chores, follow-up defects, etc.), skip this step silently. Commit directly to `develop` (`docs(roadmap): check off #<issue> (PR <number>)`) and push — `develop`'s branch protection has `enforce_admins: false`, so a direct push by the repo owner is allowed for this kind of small, reversible, docs-only change; don't open a separate PR for it.
|
|
@@ -6,7 +6,13 @@ CI (the "Lint and test" check `/merge-pr` already gates on) covers unit tests. I
|
|
|
6
6
|
|
|
7
7
|
2. **Identify the PR.** Take a PR number as an argument if given, otherwise use `gh pr list --state open` and ask the user which one if there's more than one. Get its details: `gh pr view <number> --json number,title,url,headRefName,baseRefName,statusCheckRollup,mergeable,mergeStateStatus`. If `mergeable` is `CONFLICTING`, stop here — report it and say the branch needs a rebase onto `<baseRefName>` before verification is worth doing. Don't spend code-review or live-QA effort on a branch that can't merge as-is; that cost is only worth paying once it's rebased.
|
|
8
8
|
|
|
9
|
-
3. **Check out the branch.** `gh pr checkout <number
|
|
9
|
+
3. **Check out the branch.** Don't use `gh pr checkout <number>` directly — if the worker's own worktree still has `<headRefName>` checked out (the normal case while that worker session is active), it fails with "already checked out," and the only recovery `gh` offers is unlocking/removing that worktree, which disrupts the worker for no reason. Git only blocks two worktrees from sharing the same branch *name*, not the same commit — so fetch the PR's branch into a locally-named review branch instead:
|
|
10
|
+
```
|
|
11
|
+
git fetch origin <headRefName>:review/<headRefName> --force
|
|
12
|
+
git checkout review/<headRefName>
|
|
13
|
+
git branch --set-upstream-to=origin/<headRefName> review/<headRefName>
|
|
14
|
+
```
|
|
15
|
+
This points the main checkout at the exact same commit without touching the worker's worktree at all. The `--set-upstream-to` matters: without it, `gh pr view`/`gh pr merge` with no PR number can't map the local `review/<headRefName>` branch back to the PR later (their current-branch auto-detection relies on tracked-branch info, not name matching) — `/merge-pr` should also be run with the PR number as an explicit argument here rather than relying on that auto-detection. When pushing QA-result commits later (step 9), push to the PR's real branch name via a refspec: `git push origin review/<headRefName>:<headRefName>`. Delete the local `review/<headRefName>` branch once you're done with this PR (switch away first, then `git branch -D review/<headRefName>`) so it doesn't accumulate across sessions.
|
|
10
16
|
|
|
11
17
|
4. **Reconnect the MCP server.** The server hot-reloads on file change, but this Claude session's MCP connection stays on the old process. Tell the user to run:
|
|
12
18
|
```
|
|
@@ -26,7 +32,15 @@ CI (the "Lint and test" check `/merge-pr` already gates on) covers unit tests. I
|
|
|
26
32
|
|
|
27
33
|
8. **Record results honestly.** For each test case actually executed, add `**Result (<date>) ✅ PASS**` or `**Result (<date>) ❌ FAIL — <what happened>**` directly below its `Checks` block. Never write a Result for a case you didn't actually invoke — if something blocks a case (fixture missing, tool errors before completing), write `**Result (<date>)** pending — <reason>` and say so plainly rather than guessing the outcome.
|
|
28
34
|
|
|
29
|
-
9. **Commit and push the results.** If any Result entries were added,
|
|
35
|
+
9. **Commit and push the results.** If any Result entries were added, they need to land in a real file edit. If this session runs under an isolation policy that blocks edits outside `.claude/worktrees/*` (background jobs do — regardless of which branch is checked out in the main checkout), don't spin up a fresh throwaway worktree per PR (churn — create, edit, commit, push, remove, repeat). Instead keep **one persistent scratch worktree** for the whole orchestrator session, e.g. `.claude/worktrees/qa-scratch`, and reuse it every time this step comes up:
|
|
36
|
+
```
|
|
37
|
+
git worktree add .claude/worktrees/qa-scratch -b orchestrator-qa-scratch develop # first time only
|
|
38
|
+
```
|
|
39
|
+
Its own branch name (`orchestrator-qa-scratch`) is arbitrary and never touches any PR branch, so it can never conflict with a worker's worktree. Each time this step comes up: enter it (`EnterWorktree` with `path`), `git fetch origin <headRefName> && git reset --hard origin/<headRefName>` to line it up with the PR's latest commit, make the edits, commit (`qa: record live verification for PR #<number>`), then push straight to the PR's real branch via refspec — same trick as step 3:
|
|
40
|
+
```
|
|
41
|
+
git push origin HEAD:<headRefName>
|
|
42
|
+
```
|
|
43
|
+
Ask before pushing, per this repo's normal commit/push confirmation rule. Leave the scratch worktree in place afterward (`ExitWorktree` with `action: "keep"`) for next time — don't remove it between PRs, only if the user asks for a cleanup pass.
|
|
30
44
|
|
|
31
45
|
10. **Report.** Summarize: code review findings (if any), which test cases passed/failed/pending, any coverage gaps found in step 6, and whether the branch looks ready to merge. If everything passed, say the next step is `/merge-pr`. If something failed, say the PR needs to go back to its worker session — don't attempt the fix yourself here; this session's job is verification, not implementation.
|
|
32
46
|
|
|
@@ -32,7 +32,7 @@ Tests live in `tests/` and can be run with `uv run pytest`.
|
|
|
32
32
|
Logic is split across `src/mcp_gee_sweet/`: `server.py` (MCP setup, tool decorator, resources), `auth.py` (lifespan, `SpreadsheetContext`), and `tools/` (domain-based layout):
|
|
33
33
|
|
|
34
34
|
- `tools/sheets/data.py` — read/write cell data (`get_sheet_data`, `get_sheet_formulas`, `get_multiple_*`, `find_in_spreadsheet`, `clear_values`, `update_cells`, `batch_update_cells`, `batch_update`)
|
|
35
|
-
- `tools/sheets/structure.py` — sheet structure (`list_sheets`, `copy_sheet`, `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`)
|
|
35
|
+
- `tools/sheets/structure.py` — sheet structure (`list_sheets`, `copy_sheet`, `duplicate_sheet`, `rename_sheet`, `create_sheet`, `delete_sheet`, `add_rows`, `add_columns`, `delete_rows`, `delete_columns`, `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
37
|
- `tools/drive/files.py` — file/folder operations (`create_spreadsheet`, `import_csv_to_sheet`, `list_spreadsheets`, `list_files`, `search_files`, `create_folder`, `copy_file`, `move_file`, `rename_file`, `delete_file`, `list_shared_with_me`, `list_recent_files`, `get_storage_quota`, etc.)
|
|
38
38
|
- `tools/drive/sharing.py` — permissions (`share_spreadsheet`, `share_file`, `list_permissions`, `update_permission`, `remove_permission`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev87
|
|
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
|
|
@@ -70,7 +70,7 @@ Access logs are emitted at `INFO` level — they appear when `DEBUG_LEVEL` is `D
|
|
|
70
70
|
|
|
71
71
|
<!-- Auto-generated by `scripts/gen_tool_docs.py` — do not edit by hand, your changes will be overwritten on the next commit. -->
|
|
72
72
|
|
|
73
|
-
By default all
|
|
73
|
+
By default all 93 tools are registered. Use `ENABLED_TOOLS` (or `--include-tools` on the CLI) to restrict the server to exactly the tools you need. This reduces the AI's context window cost — each registered tool is a name the model must reason about on every call.
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
# Environment variable
|
|
@@ -125,6 +125,94 @@ Tests marked **⚠️ destructive** rename or delete sheets — reset fixtures a
|
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
128
|
+
## `duplicate_sheet`
|
|
129
|
+
|
|
130
|
+
### TC-S51: Duplicate within the same spreadsheet, default name ⚠️ destructive
|
|
131
|
+
|
|
132
|
+
**Prompt**
|
|
133
|
+
> "Duplicate the Sales sheet in {SPREADSHEET_ID}"
|
|
134
|
+
|
|
135
|
+
**Checks**
|
|
136
|
+
- New tab appears, named by Google's default (e.g. "Copy of Sales")
|
|
137
|
+
- Data in the new tab matches Sales (6 rows, 4 columns)
|
|
138
|
+
- Original Sales sheet unchanged
|
|
139
|
+
- Response includes `sheetId`, `title`, `index`, `spreadsheetId`
|
|
140
|
+
|
|
141
|
+
**Result (2026-07-10) ✅ PASS**
|
|
142
|
+
`duplicate_sheet(spreadsheet_id, sheet="Sales")` → `{"sheetId":1766233601,"title":"Copy of Sales","index":1,...}`. `get_sheet_data` on "Copy of Sales" returned matching headers/rows. Deleted after verification.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### TC-S52: Duplicate with a custom name ⚠️ destructive
|
|
147
|
+
|
|
148
|
+
**Prompt**
|
|
149
|
+
> "Duplicate the Sales sheet in {SPREADSHEET_ID} and name the copy 'Sales Duplicate'"
|
|
150
|
+
|
|
151
|
+
**Checks**
|
|
152
|
+
- New tab is named 'Sales Duplicate', not Google's default
|
|
153
|
+
- `newSheetName` was passed on the same `duplicateSheet` request (single API call, no follow-up rename)
|
|
154
|
+
|
|
155
|
+
**Result (2026-07-10) ✅ PASS**
|
|
156
|
+
`duplicate_sheet(spreadsheet_id, sheet="Sales", new_name="Sales Duplicate")` → title `"Sales Duplicate"`, confirmed via code that `newSheetName` is set on the same request body as `sourceSheetId`. Deleted after verification.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### TC-S53: Default position lands immediately after the source tab ⚠️ destructive
|
|
161
|
+
|
|
162
|
+
**Prompt**
|
|
163
|
+
> "Duplicate the Sales sheet in {SPREADSHEET_ID} without specifying a position, then list all sheets"
|
|
164
|
+
|
|
165
|
+
**Checks**
|
|
166
|
+
- The new tab appears immediately after 'Sales' in the `list_sheets` response order
|
|
167
|
+
- 🔍 **Regression:** the Sheets API's own default for `duplicateSheet` places the copy at tab position 0 regardless of source position (confirmed via live QA on this tool) — the tool must compute and pass `insertSheetIndex` explicitly to land the copy after the source, matching Sheets UI's native "Duplicate" behavior
|
|
168
|
+
|
|
169
|
+
**Result (2026-07-10) ✅ PASS**
|
|
170
|
+
`duplicate_sheet(spreadsheet_id, sheet="Sales")` → index 1. `list_sheets` → `["Sales","Copy of Sales","Empty","Notes & Misc","BrandNew"]` — lands immediately after Sales, confirming the regression fix (`_get_sheet_index` + explicit `insertSheetIndex`). Deleted after verification.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### TC-S54: Explicit insert_index is honored ⚠️ destructive
|
|
175
|
+
|
|
176
|
+
**Prompt**
|
|
177
|
+
> "Duplicate the Sales sheet in {SPREADSHEET_ID}, placing the copy at tab position 0"
|
|
178
|
+
|
|
179
|
+
**Checks**
|
|
180
|
+
- The new tab is the first tab in the `list_sheets` response order
|
|
181
|
+
- Confirms an explicit `insert_index` overrides the "after source" default
|
|
182
|
+
|
|
183
|
+
**Result (2026-07-10) ✅ PASS**
|
|
184
|
+
`duplicate_sheet(spreadsheet_id, sheet="Sales", insert_index=0)` → index 0. `list_sheets` → `["Copy of Sales","Sales",...]` — explicit index correctly overrides the default. Deleted after verification.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### TC-S55: Source sheet not found
|
|
189
|
+
|
|
190
|
+
**Prompt**
|
|
191
|
+
> "Duplicate a sheet called 'DoesNotExist' in {SPREADSHEET_ID}"
|
|
192
|
+
|
|
193
|
+
**Checks**
|
|
194
|
+
- Returns `{"error": ...}` before calling the duplicate API
|
|
195
|
+
- Error references the missing sheet name
|
|
196
|
+
|
|
197
|
+
**Result (2026-07-10) ✅ PASS**
|
|
198
|
+
`duplicate_sheet(spreadsheet_id, sheet="DoesNotExist")` → `{"error": "Sheet 'DoesNotExist' not found"}`. No mutation, non-destructive.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### TC-S56: Cache invalidated after duplicate ⚠️ destructive
|
|
203
|
+
|
|
204
|
+
**Prompt**
|
|
205
|
+
> "Duplicate the Sales sheet in {SPREADSHEET_ID} as 'PostDuplicateCache', then immediately list all sheets"
|
|
206
|
+
|
|
207
|
+
**Checks**
|
|
208
|
+
- `list_sheets` includes 'PostDuplicateCache'
|
|
209
|
+
- Confirms `cache.mark_dirty(spreadsheet_id)` fires after duplicate
|
|
210
|
+
|
|
211
|
+
**Result (2026-07-10) ✅ PASS**
|
|
212
|
+
`duplicate_sheet(spreadsheet_id, sheet="Sales", new_name="PostDuplicateCache")` succeeded; immediate `list_sheets` (no explicit `refresh_cache`) included `"PostDuplicateCache"`, confirming `mark_dirty` fired. Deleted after verification; fixture restored to `["Sales","Empty","Notes & Misc","BrandNew"]`.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
128
216
|
## `rename_sheet`
|
|
129
217
|
|
|
130
218
|
### TC-S11: Rename to a new name ⚠️ destructive
|
|
@@ -121,7 +121,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
121
121
|
- [ ] `create_named_range` / `create_bookmark` — anchor points for internal links ([#152](https://github.com/khuisman/mcp-gee-sweet/issues/152))
|
|
122
122
|
- [ ] Rowspan emitter — AST `Cell.rowspan` field exists but emitter does not yet merge rows ([#195](https://github.com/khuisman/mcp-gee-sweet/issues/195))
|
|
123
123
|
- [x] Warn/error on mixed text + nested table in same cell — shipped as the fuller `Cell.children: list[Run | Table]` ordered model, also fixing trailing-text-after-nested-table ordering ([#275](https://github.com/khuisman/mcp-gee-sweet/issues/275)) in the same PR (PR #276) ([#108](https://github.com/khuisman/mcp-gee-sweet/issues/108))
|
|
124
|
-
- [
|
|
124
|
+
- [x] colspan/rowspan inside nested tables (PR #287) ([#109](https://github.com/khuisman/mcp-gee-sweet/issues/109))
|
|
125
125
|
- [ ] `find_in_doc` — search doc text and return match locations, parallel to `find_in_spreadsheet`; removes the manual index math currently needed to retrofit links (or any style) onto text already in a doc, raised while fixing #248 ([#262](https://github.com/khuisman/mcp-gee-sweet/issues/262))
|
|
126
126
|
|
|
127
127
|
**Drive**
|
|
@@ -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
|
+
93 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
|
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|---|---|---|
|
|
27
27
|
| `list_sheets` | List all sheets in a Google Spreadsheet. | `spreadsheet_id` |
|
|
28
28
|
| `copy_sheet` | Copy a sheet from one spreadsheet to another. | `src_spreadsheet`, `src_sheet`, `dst_spreadsheet`, `dst_sheet` |
|
|
29
|
+
| `duplicate_sheet` | Duplicate a sheet tab within the same spreadsheet. | `spreadsheet_id`, `sheet`, `new_name?`, `insert_index?` |
|
|
29
30
|
| `rename_sheet` | Rename a sheet in a Google Spreadsheet. | `spreadsheet`, `sheet`, `new_name` |
|
|
30
31
|
| `create_sheet` | Create a new sheet tab in an existing Google Spreadsheet. | `spreadsheet_id`, `title` |
|
|
31
32
|
| `add_rows` | Add rows to a sheet in a Google Spreadsheet. | `spreadsheet_id`, `sheet`, `count`, `start_row?` |
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/helpers.py
RENAMED
|
@@ -106,3 +106,19 @@ def _get_sheet_id(
|
|
|
106
106
|
return None
|
|
107
107
|
except Exception:
|
|
108
108
|
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_sheet_index(sheets_service: Any, spreadsheet_id: str, sheet_id: int) -> int | None:
|
|
112
|
+
"""Return the current 0-based tab position of sheet_id, or None if not found."""
|
|
113
|
+
try:
|
|
114
|
+
spreadsheet = (
|
|
115
|
+
sheets_service.spreadsheets()
|
|
116
|
+
.get(spreadsheetId=spreadsheet_id, fields="sheets.properties(sheetId,index)")
|
|
117
|
+
.execute()
|
|
118
|
+
)
|
|
119
|
+
for sheet in spreadsheet.get("sheets", []):
|
|
120
|
+
if sheet["properties"]["sheetId"] == sheet_id:
|
|
121
|
+
return sheet["properties"]["index"]
|
|
122
|
+
return None
|
|
123
|
+
except Exception:
|
|
124
|
+
return None
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/structure.py
RENAMED
|
@@ -4,7 +4,7 @@ from mcp.server.fastmcp import Context
|
|
|
4
4
|
from mcp.types import ToolAnnotations
|
|
5
5
|
|
|
6
6
|
from ...cache import fetch_sheets
|
|
7
|
-
from .helpers import _get_sheet_id, _parse_a1_notation
|
|
7
|
+
from .helpers import _get_sheet_id, _get_sheet_index, _parse_a1_notation
|
|
8
8
|
|
|
9
9
|
_VALID_CHART_TYPES = ["COLUMN", "BAR", "LINE", "AREA", "PIE", "SCATTER", "COMBO", "HISTOGRAM"]
|
|
10
10
|
|
|
@@ -124,6 +124,66 @@ def register(tool):
|
|
|
124
124
|
lc.cache.mark_dirty(dst_spreadsheet)
|
|
125
125
|
return result
|
|
126
126
|
|
|
127
|
+
@tool(annotations=ToolAnnotations(title="Duplicate Sheet", destructiveHint=True))
|
|
128
|
+
def duplicate_sheet(
|
|
129
|
+
spreadsheet_id: str,
|
|
130
|
+
sheet: str,
|
|
131
|
+
new_name: str | None = None,
|
|
132
|
+
insert_index: int | None = None,
|
|
133
|
+
ctx: Context = None,
|
|
134
|
+
) -> dict[str, Any]:
|
|
135
|
+
"""
|
|
136
|
+
Duplicate a sheet tab within the same spreadsheet.
|
|
137
|
+
|
|
138
|
+
Distinct from copy_sheet, which copies a sheet across spreadsheets.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
spreadsheet_id: The ID of the spreadsheet
|
|
142
|
+
sheet: The name of the sheet to duplicate
|
|
143
|
+
new_name: Optional title for the new sheet. Defaults to Google's
|
|
144
|
+
auto-generated name (e.g. "Copy of Sheet1") if omitted.
|
|
145
|
+
insert_index: Optional 0-based tab position for the new sheet.
|
|
146
|
+
Defaults to immediately after the source sheet if omitted.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Information about the newly created sheet
|
|
150
|
+
"""
|
|
151
|
+
lc = ctx.request_context.lifespan_context
|
|
152
|
+
sheets_service = lc.sheets_service
|
|
153
|
+
|
|
154
|
+
sheet_id = _get_sheet_id(sheets_service, spreadsheet_id, sheet, lc.cache, lc.drive_service)
|
|
155
|
+
if sheet_id is None:
|
|
156
|
+
return {"error": f"Sheet '{sheet}' not found"}
|
|
157
|
+
|
|
158
|
+
duplicate_request: dict[str, Any] = {"sourceSheetId": sheet_id}
|
|
159
|
+
if insert_index is not None:
|
|
160
|
+
duplicate_request["insertSheetIndex"] = insert_index
|
|
161
|
+
else:
|
|
162
|
+
source_index = _get_sheet_index(sheets_service, spreadsheet_id, sheet_id)
|
|
163
|
+
if source_index is not None:
|
|
164
|
+
duplicate_request["insertSheetIndex"] = source_index + 1
|
|
165
|
+
if new_name is not None:
|
|
166
|
+
duplicate_request["newSheetName"] = new_name
|
|
167
|
+
|
|
168
|
+
result = (
|
|
169
|
+
sheets_service.spreadsheets()
|
|
170
|
+
.batchUpdate(
|
|
171
|
+
spreadsheetId=spreadsheet_id,
|
|
172
|
+
body={"requests": [{"duplicateSheet": duplicate_request}]},
|
|
173
|
+
)
|
|
174
|
+
.execute()
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
lc.cache.mark_dirty(spreadsheet_id)
|
|
178
|
+
|
|
179
|
+
new_sheet_props = result["replies"][0]["duplicateSheet"]["properties"]
|
|
180
|
+
return {
|
|
181
|
+
"sheetId": new_sheet_props["sheetId"],
|
|
182
|
+
"title": new_sheet_props["title"],
|
|
183
|
+
"index": new_sheet_props.get("index"),
|
|
184
|
+
"spreadsheetId": spreadsheet_id,
|
|
185
|
+
}
|
|
186
|
+
|
|
127
187
|
@tool(annotations=ToolAnnotations(title="Rename Sheet", destructiveHint=True))
|
|
128
188
|
def rename_sheet(
|
|
129
189
|
spreadsheet: str, sheet: str, new_name: str, ctx: Context = None
|
|
@@ -237,6 +237,158 @@ class TestCopySheet:
|
|
|
237
237
|
assert not mock_sheets.spreadsheets.return_value.batchUpdate.called
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
class TestDuplicateSheet:
|
|
241
|
+
def _sheets_service(self, sheet_id=0, source_index=0, reply_props=None):
|
|
242
|
+
mock = MagicMock()
|
|
243
|
+
mock.spreadsheets.return_value.get.return_value.execute.return_value = {
|
|
244
|
+
"sheets": [
|
|
245
|
+
{"properties": {"title": "Sheet1", "sheetId": sheet_id, "index": source_index}}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
mock.spreadsheets.return_value.batchUpdate.return_value.execute.return_value = {
|
|
249
|
+
"replies": [
|
|
250
|
+
{
|
|
251
|
+
"duplicateSheet": {
|
|
252
|
+
"properties": reply_props
|
|
253
|
+
or {"sheetId": 99, "title": "Copy of Sheet1", "index": 1}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
return mock
|
|
259
|
+
|
|
260
|
+
def _cache_with_sheet(self, title="Sheet1", sheet_id=0):
|
|
261
|
+
cache = MagicMock()
|
|
262
|
+
cache.get_sheets.return_value = [SheetInfo(title=title, sheet_id=sheet_id)]
|
|
263
|
+
return cache
|
|
264
|
+
|
|
265
|
+
def _request(self, svc):
|
|
266
|
+
body = svc.spreadsheets.return_value.batchUpdate.call_args.kwargs["body"]
|
|
267
|
+
return body["requests"][0]["duplicateSheet"]
|
|
268
|
+
|
|
269
|
+
def test_sends_duplicate_sheet_request_with_source_id(self):
|
|
270
|
+
svc = self._sheets_service(sheet_id=7, source_index=0)
|
|
271
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
272
|
+
_structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
273
|
+
assert self._request(svc)["sourceSheetId"] == 7
|
|
274
|
+
|
|
275
|
+
def test_new_name_included_when_given(self):
|
|
276
|
+
svc = self._sheets_service(sheet_id=7)
|
|
277
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
278
|
+
_structure_tools["duplicate_sheet"](
|
|
279
|
+
spreadsheet_id="ss1", sheet="Sheet1", new_name="My Copy", ctx=ctx
|
|
280
|
+
)
|
|
281
|
+
assert self._request(svc)["newSheetName"] == "My Copy"
|
|
282
|
+
|
|
283
|
+
def test_insert_index_included_when_given(self):
|
|
284
|
+
svc = self._sheets_service(sheet_id=7)
|
|
285
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
286
|
+
_structure_tools["duplicate_sheet"](
|
|
287
|
+
spreadsheet_id="ss1", sheet="Sheet1", insert_index=3, ctx=ctx
|
|
288
|
+
)
|
|
289
|
+
assert self._request(svc)["insertSheetIndex"] == 3
|
|
290
|
+
|
|
291
|
+
def test_explicit_insert_index_skips_source_index_lookup(self):
|
|
292
|
+
"""When the caller supplies insert_index, no extra API call is needed to
|
|
293
|
+
compute a default — the direct spreadsheets().get() (used only for that
|
|
294
|
+
lookup) must not be invoked."""
|
|
295
|
+
svc = self._sheets_service(sheet_id=7)
|
|
296
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
297
|
+
_structure_tools["duplicate_sheet"](
|
|
298
|
+
spreadsheet_id="ss1", sheet="Sheet1", insert_index=3, ctx=ctx
|
|
299
|
+
)
|
|
300
|
+
assert not svc.spreadsheets.return_value.get.called
|
|
301
|
+
|
|
302
|
+
def test_new_name_omitted_when_not_given(self):
|
|
303
|
+
svc = self._sheets_service(sheet_id=7)
|
|
304
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
305
|
+
_structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
306
|
+
assert "newSheetName" not in self._request(svc)
|
|
307
|
+
|
|
308
|
+
def test_insert_index_defaults_to_immediately_after_source(self):
|
|
309
|
+
"""Matches Sheets' native UI 'Duplicate' behavior: the copy lands right
|
|
310
|
+
after the source tab. The Sheets API's own default (observed via live
|
|
311
|
+
QA) is index 0 regardless of source position, so this must be computed
|
|
312
|
+
and passed explicitly rather than relying on the API's default."""
|
|
313
|
+
svc = self._sheets_service(sheet_id=7, source_index=2)
|
|
314
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
315
|
+
_structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
316
|
+
assert self._request(svc)["insertSheetIndex"] == 3
|
|
317
|
+
|
|
318
|
+
def test_insert_index_omitted_when_source_index_lookup_fails(self):
|
|
319
|
+
svc = self._sheets_service(sheet_id=7)
|
|
320
|
+
svc.spreadsheets.return_value.get.return_value.execute.return_value = {"sheets": []}
|
|
321
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
322
|
+
_structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
323
|
+
assert "insertSheetIndex" not in self._request(svc)
|
|
324
|
+
|
|
325
|
+
def test_returns_new_sheet_info(self):
|
|
326
|
+
svc = self._sheets_service(
|
|
327
|
+
sheet_id=7, reply_props={"sheetId": 99, "title": "Copy of Sheet1", "index": 2}
|
|
328
|
+
)
|
|
329
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(sheet_id=7))
|
|
330
|
+
result = _structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
331
|
+
assert result == {
|
|
332
|
+
"sheetId": 99,
|
|
333
|
+
"title": "Copy of Sheet1",
|
|
334
|
+
"index": 2,
|
|
335
|
+
"spreadsheetId": "ss1",
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
def test_returns_error_when_source_sheet_not_found(self):
|
|
339
|
+
svc = self._sheets_service(sheet_id=7)
|
|
340
|
+
ctx = _make_ctx(sheets_service=svc, cache=self._cache_with_sheet(title="Other", sheet_id=7))
|
|
341
|
+
result = _structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Missing", ctx=ctx)
|
|
342
|
+
assert result == {"error": "Sheet 'Missing' not found"}
|
|
343
|
+
assert not svc.spreadsheets.return_value.batchUpdate.called
|
|
344
|
+
|
|
345
|
+
def test_marks_cache_dirty_on_success(self):
|
|
346
|
+
svc = self._sheets_service(sheet_id=7)
|
|
347
|
+
cache = self._cache_with_sheet(sheet_id=7)
|
|
348
|
+
ctx = _make_ctx(sheets_service=svc, cache=cache)
|
|
349
|
+
_structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
350
|
+
cache.mark_dirty.assert_called_with("ss1")
|
|
351
|
+
|
|
352
|
+
def test_forwards_drive_service_to_get_sheet_id(self, monkeypatch):
|
|
353
|
+
"""Regression: this call site omitted drive_service, reintroducing the
|
|
354
|
+
cache-poisoning bug fixed in #284 (modified_time silently disabled for
|
|
355
|
+
all subsequent readers of the spreadsheet)."""
|
|
356
|
+
svc = self._sheets_service(sheet_id=7)
|
|
357
|
+
cache = self._cache_with_sheet(sheet_id=7)
|
|
358
|
+
drive_svc = MagicMock()
|
|
359
|
+
ctx = _make_ctx(sheets_service=svc, cache=cache, drive_service=drive_svc)
|
|
360
|
+
mock_get_sheet_id = MagicMock(return_value=7)
|
|
361
|
+
monkeypatch.setattr(sheets_structure_module, "_get_sheet_id", mock_get_sheet_id)
|
|
362
|
+
_structure_tools["duplicate_sheet"](spreadsheet_id="ss1", sheet="Sheet1", ctx=ctx)
|
|
363
|
+
mock_get_sheet_id.assert_called_once_with(svc, "ss1", "Sheet1", cache, drive_svc)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class TestGetSheetIdCallSitesForwardDriveService:
|
|
367
|
+
"""Static regression guard: every _get_sheet_id() call in structure.py must
|
|
368
|
+
forward drive_service as its 5th argument.
|
|
369
|
+
|
|
370
|
+
PR #284 fixed cache poisoning caused by a missing drive_service (it silently
|
|
371
|
+
disables modifiedTime staleness checks for all readers of that spreadsheet).
|
|
372
|
+
duplicate_sheet reintroduced the bug by omitting the argument (caught in
|
|
373
|
+
review of PR #286) — this scans all call sites so a future tool can't drop
|
|
374
|
+
it again without a test failing.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
def test_all_call_sites_pass_drive_service(self):
|
|
378
|
+
import inspect
|
|
379
|
+
import re
|
|
380
|
+
|
|
381
|
+
source = inspect.getsource(sheets_structure_module)
|
|
382
|
+
calls = re.findall(r"_get_sheet_id\(([^)]*)\)", source)
|
|
383
|
+
assert len(calls) >= 12, (
|
|
384
|
+
f"expected at least 12 _get_sheet_id call sites, found {len(calls)}"
|
|
385
|
+
)
|
|
386
|
+
for call_args in calls:
|
|
387
|
+
assert "drive_service" in call_args, (
|
|
388
|
+
f"_get_sheet_id call missing drive_service: {call_args}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
240
392
|
class TestDeleteSheet:
|
|
241
393
|
def _sheets_service(self, sheet_id=7):
|
|
242
394
|
mock = MagicMock()
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.claude/commands/cleanup-worktrees.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.github/ISSUE_TEMPLATE/bug_report.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/.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
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-chart-theming.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-composite-tools.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/decisions/decision-docs-formatting.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/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.dev84 → mcp_gee_sweet-0.7.0.dev87}/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.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d195-create-doc.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d196-create-doc.html
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/fixtures/tc-d213-dollar-escape.md
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/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.dev84 → mcp_gee_sweet-0.7.0.dev87}/docs/qa/results/2026-07-04-drive-smoke.md
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/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
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/content.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/emitter.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/html_parser.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/layout.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/style.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/docs/tables.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/__init__.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/activity.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/files.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/sharing.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/drive/transfer.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/response_limits.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/__init__.py
RENAMED
|
File without changes
|
{mcp_gee_sweet-0.7.0.dev84 → mcp_gee_sweet-0.7.0.dev87}/src/mcp_gee_sweet/tools/sheets/data.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
|