mcp-gee-sweet 0.7.0.dev53__tar.gz → 0.7.0.dev55__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.dev53 → mcp_gee_sweet-0.7.0.dev55}/CLAUDE.md +1 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/configuration.md +1 -1
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-grid-data-size-cap.md +2 -0
- mcp_gee_sweet-0.7.0.dev55/docs/decisions/decision-response-size-cap-generalization.md +66 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/docs.md +16 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/drive.md +23 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/sheets_read.md +41 -6
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/roadmap.md +11 -10
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/tools.md +3 -3
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/.env.template +6 -4
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/content.py +41 -21
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/activity.py +13 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/transfer.py +33 -40
- mcp_gee_sweet-0.7.0.dev55/src/mcp_gee_sweet/tools/response_limits.py +45 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/sheets/data.py +97 -86
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/drive/test_activity.py +40 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/drive/test_transfer.py +46 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/sheets/test_data.py +78 -12
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_docs_content.py +53 -0
- mcp_gee_sweet-0.7.0.dev55/tests/test_response_limits.py +104 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.dockerignore +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.gitignore +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/.python-version +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/Dockerfile +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/LICENSE +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/Makefile +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/TODO.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/design.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/uv.lock +0 -0
|
@@ -48,6 +48,7 @@ Logic is split across `src/mcp_gee_sweet/`: `server.py` (MCP setup, tool decorat
|
|
|
48
48
|
- `docs/emitter.py` — AST→Docs API requests + multi-phase table fill including nested table and cell styling support
|
|
49
49
|
- `tools/cache.py` — `refresh_cache`
|
|
50
50
|
- `tools/calendar.py` — Calendar API tools
|
|
51
|
+
- `tools/response_limits.py` — shared response-size safety net (`enforce_response_size_cap`, `write_capped_result_to_disk`); imported cross-package the same way `tools/drive/__init__.py`'s `_SA_QUOTA_ERROR` is. Cap configured via `MAX_TOOL_RESPONSE_CHARS` (default 40000); see `docs/decisions/decision-response-size-cap-generalization.md`
|
|
51
52
|
|
|
52
53
|
`__init__.py` loads `src/mcp_gee_sweet/.env` via `python-dotenv` before importing `server`, so env vars from that file are in `os.environ` by the time any module-level `os.environ.get()` runs. Priority: real env var > `.env` > default.
|
|
53
54
|
|
|
@@ -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.dev55
|
|
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
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| `ENABLED_TOOLS` | Comma-separated list of tool names to register | all tools |
|
|
15
15
|
| `CACHE_DB_PATH` | Path to the SQLite cache database | `/tmp/mcp_gee_sweet.db` |
|
|
16
16
|
| `CACHE_TTL` | Cache time-to-live in seconds | `1800` (30 min) |
|
|
17
|
-
| `
|
|
17
|
+
| `MAX_TOOL_RESPONSE_CHARS` | Safety cap on response size, in characters, for tools that can return large inline payloads (`get_sheet_data`, `get_multiple_sheet_data`, `find_in_spreadsheet`, `get_doc_content`, `export_file`, `list_file_activity`). Raise to match a raised `MAX_MCP_OUTPUT_TOKENS` in your MCP client (roughly 2 characters per token observed for densely-formatted JSON) | `40000` |
|
|
18
18
|
| `HOST` | Bind address for SSE transport | `0.0.0.0` |
|
|
19
19
|
| `PORT` | Port for SSE transport | `8000` |
|
|
20
20
|
| `DEBUG_LEVEL` | Logging level: `DEBUG`, `INFO`, `WARNING`, `ERROR` — controls package and access logs | — |
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
**Date:** 2026-07-03
|
|
4
4
|
**Snapshot commit:** branch `feat/issue-235-grid-data-range` — see `src/mcp_gee_sweet/tools/sheets/data.py`
|
|
5
5
|
|
|
6
|
+
**Update (2026-07-03, issue #242):** `MAX_GRID_DATA_RESPONSE_CHARS` referenced below was renamed to `MAX_TOOL_RESPONSE_CHARS` and generalized to 5 more tools — see `docs/decisions/decision-response-size-cap-generalization.md`. This doc is left as-is below as the historical record of the original decision.
|
|
7
|
+
|
|
6
8
|
## Background
|
|
7
9
|
|
|
8
10
|
Issue #235: `get_sheet_data(include_grid_data=True)` without a `range` fetches the sheet's full padded grid (often 1000x26 by default, regardless of actual content). In the reported case, the server-side Sheets API call succeeded (`200` in under a second), but the MCP client reported "Connection closed" — the failure was downstream of this server, in the client/transport layer, with no exception for the server to catch.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Decision: Generalizing the Response-Size Safety Net (issue #242)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-03
|
|
4
|
+
**Snapshot commit:** branch `feat/issue-242-response-size-safety-net` — see `src/mcp_gee_sweet/tools/response_limits.py`
|
|
5
|
+
|
|
6
|
+
## Background
|
|
7
|
+
|
|
8
|
+
Issue #235 (see `docs/decisions/decision-grid-data-size-cap.md`) added a response-size safety net to exactly one tool, `get_sheet_data(include_grid_data=True)`: an MCP client (e.g. Claude Code, via `MAX_MCP_OUTPUT_TOKENS`) enforces a hard cap on tool response size, and exceeding it doesn't degrade gracefully — it silently kills the MCP session, requiring a full server restart to recover. That decision doc explicitly flagged #242 as the follow-up to generalize the pattern to other tools, and warned that `MAX_GRID_DATA_RESPONSE_CHARS` was scoped to one tool and wouldn't generalize as named.
|
|
9
|
+
|
|
10
|
+
This is a confirmed defect, not a hypothetical hardening gap: the silent-session-death failure mode has been observed live. What was unmapped was *which other tools* can trigger it and at what real thresholds — verifying that requires generating fixtures above the ceiling per tool, which gets most of the way to the fix anyway, so investigation and fix were scoped as one unit (`docs/roadmap.md`, v0.8.1).
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
Five more tools, in priority order by how easily they can produce an oversized response:
|
|
15
|
+
|
|
16
|
+
1. `export_file` — base64-encodes binary content inline, unbounded, no existing size control
|
|
17
|
+
2. `get_doc_content` — full unbounded plain-text export of a doc
|
|
18
|
+
3. `get_multiple_sheet_data` — unbounded query list, each returning a full value range
|
|
19
|
+
4. `find_in_spreadsheet` — bounds match *count* (`max_results`) but not match *size*
|
|
20
|
+
5. `list_file_activity` — already Drive-API-paginated and lowest risk of the five
|
|
21
|
+
|
|
22
|
+
## Decisions
|
|
23
|
+
|
|
24
|
+
### 1. Single global env var, not per-tool
|
|
25
|
+
|
|
26
|
+
Considered three options: (A) one global `MAX_TOOL_RESPONSE_CHARS` for all 6 capped tools, (B) six separate per-tool vars, (C) a hybrid — global default with a per-tool override reserved for `export_file` (whose base64 encoding inflates raw file size ~33%, making it structurally different from the others).
|
|
27
|
+
|
|
28
|
+
**Chose (A), confirmed with the user.** One knob is simpler to discover and configure than six, and the marginal precision a per-tool override buys wasn't judged worth the added config surface — an operator who needs a materially higher ceiling for `export_file` specifically can raise the shared var, or use `download_file` instead (see below).
|
|
29
|
+
|
|
30
|
+
`MAX_GRID_DATA_RESPONSE_CHARS` (from #235) was renamed to `MAX_TOOL_RESPONSE_CHARS` as part of this change — free to do since #235 hadn't shipped in a released package version yet (still pre-release on `develop`). The cap constant and check logic now live in one shared module, `src/mcp_gee_sweet/tools/response_limits.py` (`enforce_response_size_cap`, `write_capped_result_to_disk`), imported cross-package by each tool module the same way `tools/drive/__init__.py`'s `_SA_QUOTA_ERROR` already is.
|
|
31
|
+
|
|
32
|
+
### 2. `export_file` gets the cap but not `local_path`
|
|
33
|
+
|
|
34
|
+
`local_path` on the other four tools writes `json.dumps(result)` to disk as an escape hatch. For `export_file`, that would mean writing a JSON file *containing a base64 string* — a worse artifact than what already exists: `download_file` (pre-existing tool) writes the raw file bytes straight to disk with no base64 or JSON wrapping overhead. Adding a redundant, structurally worse bypass just for API symmetry with the other tools wasn't worth it. `export_file`'s cap error message points to `download_file` instead.
|
|
35
|
+
|
|
36
|
+
### 3. `list_file_activity` gets the cap, no `local_path`, no dedicated live-fixture verification
|
|
37
|
+
|
|
38
|
+
Already 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. Included the cap for defense-in-depth (cheap — one shared-helper call), but:
|
|
39
|
+
- No `local_path`: the natural remedy for "too much" here is `page_size`/pagination, not writing timeline fragments to disk.
|
|
40
|
+
- No dedicated live-fixture verification (see below): reproducing hundreds of real Drive Activity events cheaply isn't practical, and the risk is real but rare.
|
|
41
|
+
|
|
42
|
+
### 4. `get_doc_content` cache-ordering bug fixed while touched
|
|
43
|
+
|
|
44
|
+
The `doc_cache` early-return previously ran *before* any cap logic could — a cached oversized doc would bypass the cap on repeat calls. Fixed so the cap check runs on both the cache-hit and cache-miss paths (see `tests/test_docs_content.py::TestGetDocContent::test_cached_oversized_result_also_raises`).
|
|
45
|
+
|
|
46
|
+
## Live-tested thresholds
|
|
47
|
+
|
|
48
|
+
Live-verified against the real QA fixture spreadsheet/docs on 2026-07-03, after reconnecting the MCP session to this branch. Full detail in `docs/qa/tests/sheets_read.md` (TC-R34, TC-R35), `docs/qa/tests/docs.md` (TC-DOC80), `docs/qa/tests/drive.md` (TC-D167, TC-D168).
|
|
49
|
+
|
|
50
|
+
| Tool | Real fixture | Measured size | Cap tripped? |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `get_doc_content` | `TEST_LARGE_DOC_ID` grown to ~49,700 chars (seeded via `insert_doc_text`) | 49,700 chars | ✅ (fetch path and cached path both) |
|
|
53
|
+
| `export_file` (xlsx) | Small QA fixture spreadsheet (3 sheets, mostly tiny) | 54,280 chars | ✅ — even a small workbook's base64-encoded xlsx export exceeds the cap |
|
|
54
|
+
| `get_multiple_sheet_data` | 200 queries against a 6x4 range (`Sales` sheet) | 150,106 chars | ✅ — query *count* alone is a real vector, independent of per-query range size |
|
|
55
|
+
| `find_in_spreadsheet` | 10 cells x ~4,785 chars each, all matching (well under `max_results=50`) | 42,491 chars | ✅ — confirms `max_results` bounds count, not size |
|
|
56
|
+
| `list_file_activity` | N/A — no dedicated fixture, per the scoping decision above | N/A | Not live-tested; unit-tested only (defense-in-depth) |
|
|
57
|
+
|
|
58
|
+
Two findings worth calling out:
|
|
59
|
+
|
|
60
|
+
1. **`export_file` trips the cap far more readily than the others.** A workbook small enough that its *values* fit trivially under the cap still blows past it once base64-encoded — this validates the concern raised during design (item 2 above) and confirms `download_file` is the practical default, not `export_file`, for anything but the smallest files.
|
|
61
|
+
2. **`get_multiple_sheet_data`'s query-count axis is real and independent of range size** — 200 queries against a *tiny* 6x4 range (not a large one) already produced a 150K-character response. This means the cap protects against "many small things" just as much as "one big thing," which the original #235 cap (single range, single tool) never had to consider.
|
|
62
|
+
|
|
63
|
+
## When to Re-evaluate
|
|
64
|
+
|
|
65
|
+
- If a materially different MCP client (different `MAX_MCP_OUTPUT_TOKENS` or equivalent) becomes a primary consumer, the shared `40000`-character default should be re-measured, not assumed to still be well-calibrated — same caveat #235 already carries.
|
|
66
|
+
- If `export_file` usage in practice shows the shared cap is routinely too restrictive for legitimate small-to-medium binary exports, revisit the per-tool-override hybrid (option C) that was considered and set aside here.
|
|
@@ -1352,3 +1352,19 @@ These test the HTML→AST→Docs API pipeline introduced in Phase 2 (#87). All u
|
|
|
1352
1352
|
|
|
1353
1353
|
**Result (2026-06-25) ✅ PASS**
|
|
1354
1354
|
- Tool completed without error. Structure: sectionBreak → HEADING_2 "HIGH\n" (1-6) → blank para "\n" (6-7, `font_size: 1` on its run confirming collapse applied) → table (7-70, cells filled correctly: Finding/Severity/Ticket header, Some finding/HIGH/KINDLY-123 data) → trailing para (70-71). Visual check: no visible gap between heading and table in the rendered doc.
|
|
1355
|
+
|
|
1356
|
+
---
|
|
1357
|
+
|
|
1358
|
+
### TC-DOC80: get_doc_content trips the response-size cap; cached path re-checks it too (issue #242)
|
|
1359
|
+
|
|
1360
|
+
**Background:** #242 generalized #235's response-size safety net to `get_doc_content`. `doc_cache` previously returned a cached result *before* any cap check ran, so a cached oversized doc would bypass the cap on repeat calls — fixed so the check runs on both the cache-hit and cache-miss paths.
|
|
1361
|
+
|
|
1362
|
+
**Setup:** `TEST_LARGE_DOC_ID` (`mcp-gee-sweet-qa-large-doc`), grown from its original ~5,300-character seed content to ~49,700 characters by inserting repeated padding text (permanent fixture growth — this doc's whole purpose is being a large-content fixture, and it was never previously large enough to exceed any cap since none existed for this tool before now).
|
|
1363
|
+
|
|
1364
|
+
**Checks**
|
|
1365
|
+
- First call (fetch path) raises `ValueError` mentioning the actual response size, the cap, and `MAX_TOOL_RESPONSE_CHARS`
|
|
1366
|
+
- Second call (cache-hit path, no `refresh_cache` in between) raises the *same* error — proves the cache-hit path re-checks the cap rather than returning the stale oversized cached result
|
|
1367
|
+
- Same call with `local_path` set succeeds, returns `{local_path, id, bytes_written}`, and the file on disk contains the full content
|
|
1368
|
+
|
|
1369
|
+
**Result (2026-07-03) ✅ PASS**
|
|
1370
|
+
Fetch-path call raised: `get_doc_content: the response is 49700 characters, over the 40000-character safety cap. Pass local_path to write the result to disk instead of returning it inline (bypasses this cap), or set MAX_TOOL_RESPONSE_CHARS if your MCP client can handle larger responses (e.g. a raised MAX_MCP_OUTPUT_TOKENS).` Repeat call (served from `doc_cache`, confirmed via no additional Drive API round-trip) raised the identical error — confirms the cache-ordering fix. `local_path` call succeeded: `{"local_path":"/tmp/qa_doc_content_242.json","bytes_written":49700,"id":"{TEST_LARGE_DOC_ID}"}`; file verified then cleaned up.
|
|
@@ -1992,3 +1992,26 @@ Fixtures: see [`docs/qa/setup.md`](../setup.md). Substitute your `{SPREADSHEET_I
|
|
|
1992
1992
|
- When the Drive Activity API returns an HTTP 403 or 404, the tool returns `{"error": "..."}` rather than raising an exception
|
|
1993
1993
|
|
|
1994
1994
|
**Result (2026-06-24) ✅** Unit test `test_http_error_returns_error_dict` confirms HTTP errors are caught and returned as `{"error": str(e)}`.
|
|
1995
|
+
|
|
1996
|
+
---
|
|
1997
|
+
|
|
1998
|
+
### TC-D167: export_file trips the response-size cap on base64-inflated content (issue #242)
|
|
1999
|
+
|
|
2000
|
+
**Background:** #242 generalized #235's response-size safety net to `export_file`. Unlike the other capped tools, `export_file` has no `local_path` param — its base64 output written to a JSON file would be a worse artifact than `download_file` (pre-existing tool) already produces, so the error points there instead.
|
|
2001
|
+
|
|
2002
|
+
**Prompt**
|
|
2003
|
+
> "Export {SPREADSHEET_ID} as xlsx"
|
|
2004
|
+
|
|
2005
|
+
**Checks**
|
|
2006
|
+
- Call raises `ValueError` mentioning the actual response size, the 40,000-character cap, base64's ~33% inflation, and `download_file` as the recommended alternative — must NOT mention `local_path` (this tool doesn't have that param)
|
|
2007
|
+
|
|
2008
|
+
**Result (2026-07-03) ✅ PASS**
|
|
2009
|
+
Exporting even the small QA fixture spreadsheet as `xlsx` immediately exceeded the cap: `export_file: the response is 54280 characters, over the 40000-character safety cap. Base64 encoding inflates raw file size by ~33%. Call download_file instead to write the file straight to disk without this overhead, or set MAX_TOOL_RESPONSE_CHARS if your MCP client can handle larger responses (e.g. a raised MAX_MCP_OUTPUT_TOKENS).` Confirms `export_file`'s cap trips far more readily than the other capped tools given base64 inflation — `download_file` is the practical default for anything but tiny files.
|
|
2010
|
+
|
|
2011
|
+
---
|
|
2012
|
+
|
|
2013
|
+
### TC-D168: list_file_activity response-size cap — code path only, no dedicated live fixture (issue #242)
|
|
2014
|
+
|
|
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
|
+
|
|
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.
|
|
@@ -67,11 +67,11 @@ Called live against the actual Sales fixture (`gridProperties: rowCount=3016, co
|
|
|
67
67
|
**Setup**
|
|
68
68
|
Apply formatting across a range with `format_cells(spreadsheet_id={SPREADSHEET_ID}, sheet="Sales", range="A1:Z200", bold=True, background_color={"red": 0.9, "green": 0.95, "blue": 1}, number_format_type="NUMBER")`, then call `get_sheet_data(spreadsheet_id={SPREADSHEET_ID}, sheet="Sales", range="A1:Z200", include_grid_data=True)`.
|
|
69
69
|
|
|
70
|
-
Note: cell *count* alone doesn't predict this — a live test found a 26,000-cell blank range costs almost nothing (Sheets omits `rowData` for untouched cells), while densely-formatted ranges cost ~700-780 bytes/cell. The cap check runs on the actual serialized response, after the fetch, not an estimate from range size. A live binary search against Claude Code's default MCP client found the real failure point around 48,000-51,000 characters — consistent with Claude Code's documented 25,000-token default per-tool-response cap (`MAX_MCP_OUTPUT_TOKENS`) at ~2 chars/token for this kind of repeated-key JSON. The server-side default (`
|
|
70
|
+
Note: cell *count* alone doesn't predict this — a live test found a 26,000-cell blank range costs almost nothing (Sheets omits `rowData` for untouched cells), while densely-formatted ranges cost ~700-780 bytes/cell. The cap check runs on the actual serialized response, after the fetch, not an estimate from range size. A live binary search against Claude Code's default MCP client found the real failure point around 48,000-51,000 characters — consistent with Claude Code's documented 25,000-token default per-tool-response cap (`MAX_MCP_OUTPUT_TOKENS`) at ~2 chars/token for this kind of repeated-key JSON. The server-side default (`MAX_TOOL_RESPONSE_CHARS=40000`, named `MAX_GRID_DATA_RESPONSE_CHARS` at the time this test was first run — renamed and generalized to 5 more tools by issue #242) sits below that with margin.
|
|
71
71
|
|
|
72
72
|
**Checks**
|
|
73
73
|
- The Sheets API fetch itself succeeds (it's fast either way) — the error comes from the size check on the result, not a failed API call
|
|
74
|
-
- Error message states the actual response size in characters, the cap, and mentions narrowing the range, `local_path`, or `
|
|
74
|
+
- Error message states the actual response size in characters, the cap, and mentions narrowing the range, `local_path`, or `MAX_TOOL_RESPONSE_CHARS` as options
|
|
75
75
|
- Clean up: remove the formatting from A1:Z200 afterward so it doesn't affect other test cases
|
|
76
76
|
|
|
77
77
|
**Result (2026-07-02) ✅ PASS**
|
|
@@ -96,16 +96,16 @@ Same scratch-sheet setup as TC-R03c. Call returned `{"local_path": "/tmp/qa_grid
|
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
99
|
-
### TC-R03e:
|
|
99
|
+
### TC-R03e: MAX_TOOL_RESPONSE_CHARS raises the cap (issue #235)
|
|
100
100
|
|
|
101
101
|
**Setup**
|
|
102
|
-
Set `
|
|
102
|
+
Set `MAX_TOOL_RESPONSE_CHARS=200000` in server config and restart the server (e.g. to match a `MAX_MCP_OUTPUT_TOKENS` raised in your MCP client config). Repeat the same call as TC-R03c: a formatted range whose response is over the *default* 40,000-character cap but under 200,000.
|
|
103
103
|
|
|
104
104
|
**Checks**
|
|
105
105
|
- Call now succeeds instead of raising — confirms the cap is actually read from the env var, not hardcoded
|
|
106
|
-
- Restore `
|
|
106
|
+
- Restore `MAX_TOOL_RESPONSE_CHARS` (unset it) after this test
|
|
107
107
|
|
|
108
|
-
**Result:** ⏳ Pending — not yet live-tested. Unlike TC-R03b-d, this requires a server restart with a changed env var (not just an MCP reconnect), which wasn't done this pass. Covered by unit tests (`test_cap_is_configurable`, `test_env_var_sets_cap_at_import_time` in `tests/
|
|
108
|
+
**Result:** ⏳ Pending — not yet live-tested. Unlike TC-R03b-d, this requires a server restart with a changed env var (not just an MCP reconnect), which wasn't done this pass. Covered by unit tests (`test_cap_is_configurable` in `tests/sheets/test_data.py`, `test_env_var_sets_cap_at_import_time` in `tests/test_response_limits.py`) in the meantime. Var renamed from `MAX_GRID_DATA_RESPONSE_CHARS` by issue #242 (generalized to 5 more tools).
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
@@ -491,3 +491,38 @@ Clear `userEnteredFormat` from B2.
|
|
|
491
491
|
|
|
492
492
|
**Result (2026-06-20) ✅ PASS**
|
|
493
493
|
- `effectiveFormat.numberFormat = {type:"CURRENCY", pattern:"$#,##0.00"}`, `formattedValue = "$100.00"`, `effectiveValue.numberValue = 100`
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
### TC-R34: get_multiple_sheet_data — many small queries trips the response-size cap (issue #242)
|
|
498
|
+
|
|
499
|
+
**Background:** #242 generalized #235's response-size safety net beyond `get_sheet_data`. For `get_multiple_sheet_data`, the unbounded axis is query *count*, not just per-query range size — many small results can add up.
|
|
500
|
+
|
|
501
|
+
**Setup**
|
|
502
|
+
No fixture setup needed — repeat the same tiny query (`{spreadsheet_id: {SPREADSHEET_ID}, sheet: "Sales"}`, a 6x4 range) 200 times in one `queries` list.
|
|
503
|
+
|
|
504
|
+
**Checks**
|
|
505
|
+
- Call raises `ValueError` mentioning the actual response size, the 40,000-character cap, and `MAX_TOOL_RESPONSE_CHARS`
|
|
506
|
+
- Same call with only 2 queries + `local_path` set succeeds, returns `{local_path, query_count, bytes_written}`, and the file on disk contains the full per-query results
|
|
507
|
+
|
|
508
|
+
**Result (2026-07-03) ✅ PASS**
|
|
509
|
+
200 queries against the small `Sales` range (6 rows x 4 cols) raised: `get_multiple_sheet_data: the response is 150106 characters, over the 40000-character safety cap. Pass local_path to write the result to disk instead of returning it inline (bypasses this cap), or set MAX_TOOL_RESPONSE_CHARS if your MCP client can handle larger responses (e.g. a raised MAX_MCP_OUTPUT_TOKENS).` `local_path` with 2 queries succeeded, returned `{"local_path":"/tmp/qa_multiple_sheet_data_242.json","bytes_written":2048,"query_count":2}`; file verified on disk then cleaned up.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### TC-R35: find_in_spreadsheet — max_results bounds count, not size (issue #242)
|
|
514
|
+
|
|
515
|
+
**Background:** `max_results` (default 50) caps how many matches are returned, not how large each matched cell value is. A handful of large matching cells can exceed the response-size cap even with few matches.
|
|
516
|
+
|
|
517
|
+
**Setup**
|
|
518
|
+
Write 10 cells (`Empty!A1:A10`) each containing a ~4,785-character string with a shared marker substring (e.g. repeated "PADTEST marker text..." sentence), then search for the marker.
|
|
519
|
+
|
|
520
|
+
**Checks**
|
|
521
|
+
- Call raises `ValueError` mentioning the actual response size and the cap, even though match count (10) is well under `max_results` (50)
|
|
522
|
+
- Same call with `local_path` set succeeds, returns `{local_path, spreadsheet_id, query, match_count, bytes_written}`, and the file on disk contains all matches
|
|
523
|
+
|
|
524
|
+
**Teardown**
|
|
525
|
+
`clear_values` on the `Empty` sheet to remove the test data.
|
|
526
|
+
|
|
527
|
+
**Result (2026-07-03) ✅ PASS**
|
|
528
|
+
10 matches (42,491 chars total) raised: `find_in_spreadsheet: the response is 42491 characters, over the 40000-character safety cap. ...` despite being far under the `max_results=50` default — confirming match-count capping alone doesn't bound response size. `local_path` call succeeded: `{"local_path":"/tmp/qa_find_in_spreadsheet_242.json","bytes_written":42491,"spreadsheet_id":"...","query":"PADTEST","match_count":10}`; file verified then cleaned up. Test data cleared from the `Empty` sheet afterward.
|
|
@@ -31,12 +31,12 @@ Features are ordered by practical priority within each tier, cross-domain. Items
|
|
|
31
31
|
|
|
32
32
|
| Version | Scope | Signal |
|
|
33
33
|
|---------|-------|--------|
|
|
34
|
-
| **v0.7.0** | ✅ First stable PyPI release — 63 tools across Sheets, Drive, Docs, Calendar | Published 2026-06-21 |
|
|
35
|
-
| **v0.8.0** | ✅ Tier 1 complete — all "frequently needed" items across all domains (84 tools) | Published 2026-06-29 |
|
|
36
|
-
| **v0.8.1** | Defect & documentation cleanup — no new tools, ships the QA/refactor work already on `develop` plus fixes for #235, #213, #239, #236 | Stabilizes before Tier 2 feature work begins |
|
|
37
|
-
| **v0.9.0** | Tier 2 complete — power-user and structured-work layer, features only (~20 tools) | Covers most real workflows |
|
|
38
|
-
| **v1.0.0** | API stability declaration — Tier 3 items that make the cut + any breaking cleanups from v0.8–0.9 | Backwards-compatibility commitment |
|
|
39
|
-
| **v1.1.0+** | Future domains — Tasks, Gmail (separate minor releases, each needs a new API client) | Expanded scope |
|
|
34
|
+
| [**v0.7.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.7) | ✅ First stable PyPI release — 63 tools across Sheets, Drive, Docs, Calendar | Published 2026-06-21 |
|
|
35
|
+
| [**v0.8.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.8) | ✅ Tier 1 complete — all "frequently needed" items across all domains (84 tools) | Published 2026-06-29 |
|
|
36
|
+
| [**v0.8.1**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.8.1) | Defect & documentation cleanup — no new tools, ships the QA/refactor work already on `develop` plus fixes for #235, #242, #213, #239, #236 | Stabilizes before Tier 2 feature work begins |
|
|
37
|
+
| [**v0.9.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9) | Tier 2 complete — power-user and structured-work layer, features only (~20 tools) | Covers most real workflows |
|
|
38
|
+
| [**v1.0.0**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av1.0) | API stability declaration — Tier 3 items that make the cut + any breaking cleanups from v0.8–0.9 | Backwards-compatibility commitment |
|
|
39
|
+
| [**v1.1.0+**](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3A%22v1.1%2B%22) | Future domains — Tasks, Gmail (separate minor releases, each needs a new API client) | Expanded scope |
|
|
40
40
|
|
|
41
41
|
Tier 4 items remain backlog with no assigned version.
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ Tier 4 items remain backlog with no assigned version.
|
|
|
44
44
|
|
|
45
45
|
## Roadmap
|
|
46
46
|
|
|
47
|
-
### Tier 1 — High value, frequently needed _(target: v0.8.0)_
|
|
47
|
+
### Tier 1 — High value, frequently needed _(target: [v0.8.0](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.8))_
|
|
48
48
|
|
|
49
49
|
**Infrastructure**
|
|
50
50
|
- [x] PyPI publish — OIDC trusted publishing ([#55](https://github.com/khuisman/mcp-gee-sweet/issues/55)) — v0.7.0 stable live; `uvx mcp-gee-sweet` works
|
|
@@ -71,7 +71,7 @@ Tier 4 items remain backlog with no assigned version.
|
|
|
71
71
|
- [x] `list_recent_files` — files recently accessed or modified ([#136](https://github.com/khuisman/mcp-gee-sweet/issues/136))
|
|
72
72
|
- [x] `get_storage_quota` — Drive storage usage and limits ([#137](https://github.com/khuisman/mcp-gee-sweet/issues/137))
|
|
73
73
|
|
|
74
|
-
### v0.8.1 — Defect & documentation cleanup _(target: v0.8.1, before Tier 2 begins)_
|
|
74
|
+
### v0.8.1 — Defect & documentation cleanup _(target: [v0.8.1](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.8.1), before Tier 2 begins)_
|
|
75
75
|
|
|
76
76
|
No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature work.
|
|
77
77
|
|
|
@@ -83,6 +83,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
83
83
|
|
|
84
84
|
**Remaining defects to fix before cutting the release**
|
|
85
85
|
- [ ] `get_sheet_data(include_grid_data=True)` without a range fetches the full padded grid instead of the used range ([#235](https://github.com/khuisman/mcp-gee-sweet/issues/235))
|
|
86
|
+
- [x] Generalize the #235 response-size safety net (post-fetch size check + configurable cap + `local_path` bypass) to other tools that can return large responses — `export_file`, `get_doc_content`, `get_multiple_sheet_data`, `find_in_spreadsheet`, `list_file_activity`. Oversized responses silently kill the MCP session today (client-side output cap, e.g. Claude Code's `MAX_MCP_OUTPUT_TOKENS`), requiring a server restart to recover — confirmed live, not hypothetical. Renamed `MAX_GRID_DATA_RESPONSE_CHARS` to `MAX_TOOL_RESPONSE_CHARS` (single global cap); live-verified thresholds per tool ([#242](https://github.com/khuisman/mcp-gee-sweet/issues/242))
|
|
86
87
|
- [ ] `create_doc_from_file` markdown `$` escape renders as literal backslash+dollar in the doc ([#213](https://github.com/khuisman/mcp-gee-sweet/issues/213))
|
|
87
88
|
- [ ] Unknown tool parameters are silently dropped instead of raising a validation error — FastMCP's generated pydantic arg models default to ignoring extra fields, so a typo'd kwarg (e.g. `parent_id` instead of `parent_folder_id`) silently falls back to default behavior instead of erroring ([#239](https://github.com/khuisman/mcp-gee-sweet/issues/239))
|
|
88
89
|
- [x] Auto-generate `docs/tools.md` from tool source as a pre-commit hook — makes the manual tools.md backfill in #236 unnecessary (PR #240) ([#94](https://github.com/khuisman/mcp-gee-sweet/issues/94))
|
|
@@ -90,7 +91,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
90
91
|
- [ ] Rewrite `CONTRIBUTING.md` — fix broken links to removed README anchors, fix stale "Available Tool Names" and `bug`-label references, add better local-dev setup examples including observability (`DEBUG_LEVEL`/`LOG_FILE`/`ACCESS_LOG_FILE`) ([#95](https://github.com/khuisman/mcp-gee-sweet/issues/95))
|
|
91
92
|
- [ ] Define and document community PR expectations — template, testing bar for non-tool PRs, scope/size convention, CLA/DCO/code-of-conduct decision, review turnaround ([#237](https://github.com/khuisman/mcp-gee-sweet/issues/237))
|
|
92
93
|
|
|
93
|
-
### Tier 2 — Useful for structured work, features only _(target: v0.9.0)_
|
|
94
|
+
### Tier 2 — Useful for structured work, features only _(target: [v0.9.0](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av0.9))_
|
|
94
95
|
|
|
95
96
|
**Sheets**
|
|
96
97
|
- [ ] `update_borders` — border style, width, color on a range ([#122](https://github.com/khuisman/mcp-gee-sweet/issues/122)) _(freema/mcp-gsheets)_
|
|
@@ -129,7 +130,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
129
130
|
**Infrastructure**
|
|
130
131
|
- [ ] Cache reliability & configurability — runtime-configurable TTL, smarter invalidation for shared files ([#99](https://github.com/khuisman/mcp-gee-sweet/issues/99)); harden concurrent-session access — fail-open on cache read/write errors, `busy_timeout` ([#234](https://github.com/khuisman/mcp-gee-sweet/issues/234))
|
|
131
132
|
|
|
132
|
-
### Tier 3 — Advanced / occasionally needed _(target: v1.0.0)_
|
|
133
|
+
### Tier 3 — Advanced / occasionally needed _(target: [v1.0.0](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3Av1.0))_
|
|
133
134
|
|
|
134
135
|
**Sheets**
|
|
135
136
|
- [ ] `add_conditional_formatting` / `delete_conditional_formatting` ([#129](https://github.com/khuisman/mcp-gee-sweet/issues/129)) _(freema/mcp-gsheets)_
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| `get_sheet_data` | Get data from a specific sheet in a Google Spreadsheet. | `spreadsheet_id`, `sheet`, `range?`, `include_grid_data?`, `local_path?` |
|
|
12
12
|
| `get_sheet_formulas` | Get formulas from a specific sheet in a Google Spreadsheet. | `spreadsheet_id`, `sheet`, `range?` |
|
|
13
|
-
| `get_multiple_sheet_data` | Get data from multiple specific ranges in Google Spreadsheets. | `queries` |
|
|
13
|
+
| `get_multiple_sheet_data` | Get data from multiple specific ranges in Google Spreadsheets. | `queries`, `local_path?` |
|
|
14
14
|
| `get_multiple_spreadsheet_summary` | Get a summary of multiple Google Spreadsheets, including sheet names, headers, and the first few rows of data for each sheet. | `spreadsheet_ids`, `rows_to_fetch?` |
|
|
15
|
-
| `find_in_spreadsheet` | Find cells containing a specific value in a Google Spreadsheet. | `spreadsheet_id`, `query`, `sheet?`, `case_sensitive?`, `max_results?` |
|
|
15
|
+
| `find_in_spreadsheet` | Find cells containing a specific value in a Google Spreadsheet. | `spreadsheet_id`, `query`, `sheet?`, `case_sensitive?`, `max_results?`, `local_path?` |
|
|
16
16
|
| `clear_values` | Clear cell content in a range without touching formatting. | `spreadsheet_id`, `sheet`, `range?` |
|
|
17
17
|
| `update_cells` | Update cells in a Google Spreadsheet. | `spreadsheet_id`, `sheet`, `range`, `data` |
|
|
18
18
|
| `batch_update_cells` | Batch update multiple ranges in a Google Spreadsheet. | `spreadsheet_id`, `sheet`, `ranges` |
|
|
@@ -112,7 +112,7 @@ Requires the `drive.activity.readonly` scope; uses the Drive Activity API v2.
|
|
|
112
112
|
|---|---|---|
|
|
113
113
|
| `create_doc` | Create a new Google Doc, optionally with initial content. | `title`, `content?`, `folder_id?`, `content_format?` |
|
|
114
114
|
| `create_doc_from_file` | Create a Google Doc from a local .md or .html file. | `local_path`, `title?`, `folder_id?` |
|
|
115
|
-
| `get_doc_content` | Get the plain text content of a Google Doc. | `file_id` |
|
|
115
|
+
| `get_doc_content` | Get the plain text content of a Google Doc. | `file_id`, `local_path?` |
|
|
116
116
|
| `write_doc_content` | Replace the full content of an existing Google Doc. | `doc_id`, `content`, `content_format?` |
|
|
117
117
|
| `get_doc_structure` | Return the full structural map of a Google Doc body with element indices. | `doc_id` |
|
|
118
118
|
| `insert_doc_text` | Insert text at one or more positions in a Google Doc. | `doc_id`, `insertions` |
|
|
@@ -29,10 +29,12 @@ ACCESS_LOG_FILE=/tmp/mcp-gee-sweet-access.log
|
|
|
29
29
|
# CACHE_TTL=1800
|
|
30
30
|
# CACHE_DB_PATH=/tmp/mcp_gee_sweet.db
|
|
31
31
|
|
|
32
|
-
# Safety cap on
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
32
|
+
# Safety cap on response size, in characters, for tools that can return large inline
|
|
33
|
+
# payloads (get_sheet_data, get_multiple_sheet_data, find_in_spreadsheet,
|
|
34
|
+
# get_doc_content, export_file, list_file_activity). Raise to match a raised
|
|
35
|
+
# MAX_MCP_OUTPUT_TOKENS in your MCP client (~2 chars/token observed for densely
|
|
36
|
+
# formatted JSON — see docs/qa/tests/sheets_read.md TC-R03c/d).
|
|
37
|
+
# MAX_TOOL_RESPONSE_CHARS=40000
|
|
36
38
|
|
|
37
39
|
# QA OAuth automation (scripts/oauth_setup.py — see docs/qa/playwright_oauth.md)
|
|
38
40
|
# GOOGLE_TEST_EMAIL=
|
{mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/docs/content.py
RENAMED
|
@@ -10,6 +10,7 @@ from mcp.server.fastmcp import Context
|
|
|
10
10
|
from mcp.types import ToolAnnotations
|
|
11
11
|
|
|
12
12
|
from ..drive import _SA_QUOTA_ERROR
|
|
13
|
+
from ..response_limits import enforce_response_size_cap, write_capped_result_to_disk
|
|
13
14
|
from .ast import Table
|
|
14
15
|
from .emitter import ast_to_requests, fill_tables
|
|
15
16
|
from .html_parser import html_to_ast
|
|
@@ -251,44 +252,63 @@ def register(tool):
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
@tool(annotations=ToolAnnotations(title="Get Document Content", readOnlyHint=True))
|
|
254
|
-
def get_doc_content(
|
|
255
|
+
def get_doc_content(
|
|
256
|
+
file_id: str, local_path: str | None = None, ctx: Context = None
|
|
257
|
+
) -> dict[str, Any]:
|
|
255
258
|
"""
|
|
256
259
|
Get the plain text content of a Google Doc.
|
|
257
260
|
|
|
258
261
|
Args:
|
|
259
262
|
file_id: The Google Drive file ID of the document.
|
|
263
|
+
local_path: Optional local filesystem path (file or directory) to write the
|
|
264
|
+
result to instead of returning it inline. Bypasses the response-size cap.
|
|
265
|
+
Same caveat as download_file/download_folder: this path is resolved on the
|
|
266
|
+
*server's* filesystem, not the caller's.
|
|
260
267
|
|
|
261
268
|
Returns:
|
|
262
269
|
Dictionary with the document's text content and metadata. Results are
|
|
263
270
|
cached; call refresh_cache(doc_id=file_id) to invalidate, or
|
|
264
|
-
refresh_cache() to clear all caches.
|
|
271
|
+
refresh_cache() to clear all caches. Raises ValueError if the response
|
|
272
|
+
exceeds a safety cap (default 40,000 characters, set MAX_TOOL_RESPONSE_CHARS
|
|
273
|
+
to change it) and local_path is not set. If local_path is set, returns
|
|
274
|
+
{local_path, id, bytes_written} instead.
|
|
265
275
|
"""
|
|
266
276
|
lc = ctx.request_context.lifespan_context
|
|
267
277
|
drive_service = lc.drive_service
|
|
268
278
|
doc_cache = lc.doc_cache
|
|
269
279
|
|
|
270
|
-
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
result = doc_cache.get(file_id)
|
|
281
|
+
if result is None:
|
|
282
|
+
metadata = (
|
|
283
|
+
drive_service.files()
|
|
284
|
+
.get(
|
|
285
|
+
fileId=file_id,
|
|
286
|
+
fields="id, name, modifiedTime, webViewLink",
|
|
287
|
+
supportsAllDrives=True,
|
|
288
|
+
)
|
|
289
|
+
.execute()
|
|
278
290
|
)
|
|
279
|
-
.execute()
|
|
280
|
-
)
|
|
281
291
|
|
|
282
|
-
|
|
292
|
+
content = drive_service.files().export(fileId=file_id, mimeType="text/plain").execute()
|
|
283
293
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
result = {
|
|
295
|
+
"id": metadata["id"],
|
|
296
|
+
"name": metadata["name"],
|
|
297
|
+
"modified_time": metadata.get("modifiedTime"),
|
|
298
|
+
"web_link": metadata.get("webViewLink"),
|
|
299
|
+
"content": content.decode("utf-8") if isinstance(content, bytes) else content,
|
|
300
|
+
}
|
|
301
|
+
doc_cache.store(file_id, result)
|
|
302
|
+
|
|
303
|
+
if local_path:
|
|
304
|
+
return write_capped_result_to_disk(
|
|
305
|
+
result,
|
|
306
|
+
local_path,
|
|
307
|
+
default_filename=f"{file_id}_content.json",
|
|
308
|
+
manifest_extra={"id": file_id},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
enforce_response_size_cap(result, tool_name="get_doc_content")
|
|
292
312
|
return result
|
|
293
313
|
|
|
294
314
|
@tool(annotations=ToolAnnotations(title="Write Document Content", destructiveHint=True))
|
{mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/activity.py
RENAMED
|
@@ -5,6 +5,8 @@ from googleapiclient.errors import HttpError
|
|
|
5
5
|
from mcp.server.fastmcp import Context
|
|
6
6
|
from mcp.types import ToolAnnotations
|
|
7
7
|
|
|
8
|
+
from ..response_limits import enforce_response_size_cap
|
|
9
|
+
|
|
8
10
|
logger = logging.getLogger(__name__)
|
|
9
11
|
|
|
10
12
|
# Maps Drive Activity API primaryActionDetail keys to human-readable action types.
|
|
@@ -83,6 +85,10 @@ def register(tool):
|
|
|
83
85
|
- file_id: echoed back for reference
|
|
84
86
|
- activities: list of activity entries (timestamp, action, actors)
|
|
85
87
|
- next_page_token: present when more results are available
|
|
88
|
+
Raises ValueError if the response exceeds a safety cap (default 40,000
|
|
89
|
+
characters, set MAX_TOOL_RESPONSE_CHARS to change it) — lower page_size and
|
|
90
|
+
paginate via next_page_token instead of raising the cap; a single activity's
|
|
91
|
+
actors list can be large on files with many collaborators.
|
|
86
92
|
|
|
87
93
|
Note:
|
|
88
94
|
The Drive Activity API requires the drive.activity.readonly OAuth scope.
|
|
@@ -118,5 +124,12 @@ def register(tool):
|
|
|
118
124
|
if npt := response.get("nextPageToken"):
|
|
119
125
|
result["next_page_token"] = npt
|
|
120
126
|
|
|
127
|
+
enforce_response_size_cap(
|
|
128
|
+
result,
|
|
129
|
+
tool_name="list_file_activity",
|
|
130
|
+
hint="Lower page_size and paginate via next_page_token, or ",
|
|
131
|
+
local_path_available=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
121
134
|
logger.debug("list_file_activity: %d activities for file %s", len(activities), file_id)
|
|
122
135
|
return result
|
{mcp_gee_sweet-0.7.0.dev53 → mcp_gee_sweet-0.7.0.dev55}/src/mcp_gee_sweet/tools/drive/transfer.py
RENAMED
|
@@ -12,6 +12,7 @@ from googleapiclient.http import MediaFileUpload, MediaInMemoryUpload, MediaIoBa
|
|
|
12
12
|
from mcp.server.fastmcp import Context
|
|
13
13
|
from mcp.types import ToolAnnotations
|
|
14
14
|
|
|
15
|
+
from ..response_limits import enforce_response_size_cap
|
|
15
16
|
from . import _SA_QUOTA_ERROR
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -80,7 +81,12 @@ def register(tool):
|
|
|
80
81
|
Returns:
|
|
81
82
|
fileId, name, mime_type, format, encoding ('utf-8' or 'base64'), content.
|
|
82
83
|
Text formats (txt, html, csv, rtf) are returned as plain strings; all others
|
|
83
|
-
are base64-encoded bytes.
|
|
84
|
+
are base64-encoded bytes. Raises ValueError if the response exceeds a safety
|
|
85
|
+
cap (default 40,000 characters, set MAX_TOOL_RESPONSE_CHARS to change it) —
|
|
86
|
+
base64 encoding inflates raw file size by ~33%, so binary exports hit this
|
|
87
|
+
cap at a much smaller *file* size than text ones. Call download_file instead
|
|
88
|
+
for anything but small files; it writes raw bytes straight to disk with no
|
|
89
|
+
base64/JSON overhead.
|
|
84
90
|
"""
|
|
85
91
|
_TEXT_MIME_PREFIXES = ("text/",)
|
|
86
92
|
|
|
@@ -102,53 +108,40 @@ def register(tool):
|
|
|
102
108
|
while not done:
|
|
103
109
|
_, done = downloader.next_chunk()
|
|
104
110
|
raw_bytes = buf.getvalue()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"fileId": file_id,
|
|
117
|
-
"name": metadata["name"],
|
|
118
|
-
"mime_type": file_mime,
|
|
119
|
-
"format": export_format,
|
|
120
|
-
"encoding": "base64",
|
|
121
|
-
"content": base64.b64encode(raw_bytes).decode("ascii"),
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if export_format not in _EXPORT_MIME:
|
|
125
|
-
raise ValueError(
|
|
126
|
-
f"Unknown export_format '{export_format}'. "
|
|
127
|
-
f"Valid options: {', '.join(_EXPORT_MIME)}, raw"
|
|
111
|
+
target_mime = file_mime
|
|
112
|
+
content_bytes = raw_bytes
|
|
113
|
+
else:
|
|
114
|
+
if export_format not in _EXPORT_MIME:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Unknown export_format '{export_format}'. "
|
|
117
|
+
f"Valid options: {', '.join(_EXPORT_MIME)}, raw"
|
|
118
|
+
)
|
|
119
|
+
target_mime = _EXPORT_MIME[export_format][0]
|
|
120
|
+
content_bytes = (
|
|
121
|
+
drive_service.files().export(fileId=file_id, mimeType=target_mime).execute()
|
|
128
122
|
)
|
|
129
|
-
|
|
123
|
+
if isinstance(content_bytes, str):
|
|
124
|
+
content_bytes = content_bytes.encode("utf-8")
|
|
130
125
|
|
|
131
|
-
content_bytes = drive_service.files().export(fileId=file_id, mimeType=target_mime).execute()
|
|
132
126
|
is_text = any(target_mime.startswith(p) for p in _TEXT_MIME_PREFIXES)
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
"fileId": file_id,
|
|
136
|
-
"name": metadata["name"],
|
|
137
|
-
"mime_type": target_mime,
|
|
138
|
-
"format": export_format,
|
|
139
|
-
"encoding": "utf-8",
|
|
140
|
-
"content": content_bytes.decode("utf-8")
|
|
141
|
-
if isinstance(content_bytes, bytes)
|
|
142
|
-
else content_bytes,
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
127
|
+
result = {
|
|
145
128
|
"fileId": file_id,
|
|
146
129
|
"name": metadata["name"],
|
|
147
130
|
"mime_type": target_mime,
|
|
148
131
|
"format": export_format,
|
|
149
|
-
"encoding": "base64",
|
|
150
|
-
"content":
|
|
132
|
+
"encoding": "utf-8" if is_text else "base64",
|
|
133
|
+
"content": content_bytes.decode("utf-8", errors="replace")
|
|
134
|
+
if is_text
|
|
135
|
+
else base64.b64encode(content_bytes).decode("ascii"),
|
|
151
136
|
}
|
|
137
|
+
enforce_response_size_cap(
|
|
138
|
+
result,
|
|
139
|
+
tool_name="export_file",
|
|
140
|
+
hint="Base64 encoding inflates raw file size by ~33%. Call download_file "
|
|
141
|
+
"instead to write the file straight to disk without this overhead, or ",
|
|
142
|
+
local_path_available=False,
|
|
143
|
+
)
|
|
144
|
+
return result
|
|
152
145
|
|
|
153
146
|
@tool(annotations=ToolAnnotations(title="List Revisions", readOnlyHint=True))
|
|
154
147
|
def list_revisions(file_id: str, ctx: Context = None) -> list[dict[str, Any]]:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# See docs/decisions/decision-grid-data-size-cap.md and
|
|
7
|
+
# docs/decisions/decision-response-size-cap-generalization.md for the live-tested
|
|
8
|
+
# numbers behind this default and cap/local_path design.
|
|
9
|
+
MAX_TOOL_RESPONSE_CHARS = int(os.environ.get("MAX_TOOL_RESPONSE_CHARS", "40000"))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def enforce_response_size_cap(
|
|
13
|
+
result: Any, *, tool_name: str, hint: str = "", local_path_available: bool = True
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Raise ValueError if the serialized result exceeds the configured cap."""
|
|
16
|
+
size = len(json.dumps(result))
|
|
17
|
+
if size > MAX_TOOL_RESPONSE_CHARS:
|
|
18
|
+
local_path_clause = (
|
|
19
|
+
"Pass local_path to write the result to disk instead of returning it inline "
|
|
20
|
+
"(bypasses this cap), or set"
|
|
21
|
+
if local_path_available
|
|
22
|
+
else "set"
|
|
23
|
+
)
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"{tool_name}: the response is {size} characters, over the "
|
|
26
|
+
f"{MAX_TOOL_RESPONSE_CHARS}-character safety cap. {hint}"
|
|
27
|
+
f"{local_path_clause} MAX_TOOL_RESPONSE_CHARS if your MCP client can "
|
|
28
|
+
"handle larger responses (e.g. a raised MAX_MCP_OUTPUT_TOKENS)."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_capped_result_to_disk(
|
|
33
|
+
result: Any, local_path: str, *, default_filename: str, manifest_extra: dict[str, Any]
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Write result (dict or list) to local_path and return a manifest dict.
|
|
36
|
+
|
|
37
|
+
Resolves a directory path via default_filename, mirroring get_sheet_data's
|
|
38
|
+
original local_path handling from issue #235.
|
|
39
|
+
"""
|
|
40
|
+
dest = Path(local_path)
|
|
41
|
+
if dest.is_dir():
|
|
42
|
+
dest = dest / default_filename
|
|
43
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
dest.write_text(json.dumps(result))
|
|
45
|
+
return {"local_path": str(dest), "bytes_written": dest.stat().st_size, **manifest_extra}
|