flin-bigquery-mcp 0.1.0__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.
- flin_bigquery_mcp-0.1.0/.env.example +13 -0
- flin_bigquery_mcp-0.1.0/.github/workflows/ci.yml +27 -0
- flin_bigquery_mcp-0.1.0/.github/workflows/release.yml +76 -0
- flin_bigquery_mcp-0.1.0/.gitignore +17 -0
- flin_bigquery_mcp-0.1.0/LICENSE +21 -0
- flin_bigquery_mcp-0.1.0/PKG-INFO +192 -0
- flin_bigquery_mcp-0.1.0/PRIVACY.md +9 -0
- flin_bigquery_mcp-0.1.0/README.md +168 -0
- flin_bigquery_mcp-0.1.0/SUPPORT.md +5 -0
- flin_bigquery_mcp-0.1.0/assets/icon.png +0 -0
- flin_bigquery_mcp-0.1.0/assets/icon.svg +5 -0
- flin_bigquery_mcp-0.1.0/docs/release.md +13 -0
- flin_bigquery_mcp-0.1.0/docs/testing.md +15 -0
- flin_bigquery_mcp-0.1.0/mcpb/manifest.template.json +142 -0
- flin_bigquery_mcp-0.1.0/mcpb/server.py +6 -0
- flin_bigquery_mcp-0.1.0/pyproject.toml +43 -0
- flin_bigquery_mcp-0.1.0/schemas/LICENSE.mcpb +7 -0
- flin_bigquery_mcp-0.1.0/schemas/README.md +5 -0
- flin_bigquery_mcp-0.1.0/schemas/mcpb-manifest-v0.4.schema.json +377 -0
- flin_bigquery_mcp-0.1.0/scripts/build_mcpb.py +154 -0
- flin_bigquery_mcp-0.1.0/scripts/check_mcpb.py +127 -0
- flin_bigquery_mcp-0.1.0/scripts/check_release.py +24 -0
- flin_bigquery_mcp-0.1.0/src/flin_bigquery_mcp/__init__.py +3 -0
- flin_bigquery_mcp-0.1.0/src/flin_bigquery_mcp/auth.py +251 -0
- flin_bigquery_mcp-0.1.0/src/flin_bigquery_mcp/bigquery.py +402 -0
- flin_bigquery_mcp-0.1.0/src/flin_bigquery_mcp/config.py +166 -0
- flin_bigquery_mcp-0.1.0/src/flin_bigquery_mcp/server.py +296 -0
- flin_bigquery_mcp-0.1.0/tests/conftest.py +10 -0
- flin_bigquery_mcp-0.1.0/tests/test_auth.py +375 -0
- flin_bigquery_mcp-0.1.0/tests/test_bigquery.py +240 -0
- flin_bigquery_mcp-0.1.0/tests/test_config.py +243 -0
- flin_bigquery_mcp-0.1.0/tests/test_mcpb.py +82 -0
- flin_bigquery_mcp-0.1.0/tests/test_sdk_integration.py +97 -0
- flin_bigquery_mcp-0.1.0/tests/test_server.py +73 -0
- flin_bigquery_mcp-0.1.0/uv.lock +1791 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Desktop OAuth (same setup as other flin Google MCPs)
|
|
2
|
+
GOOGLE_CLIENT_ID=
|
|
3
|
+
GOOGLE_CLIENT_SECRET=
|
|
4
|
+
GOOGLE_BIGQUERY_PROJECT=
|
|
5
|
+
GOOGLE_BIGQUERY_LOCATION=EU
|
|
6
|
+
# oauth (default) or adc (Application Default Credentials)
|
|
7
|
+
GOOGLE_BIGQUERY_AUTH_MODE=oauth
|
|
8
|
+
# Hard per-query ceiling: 1 GiB. Tool calls may lower, never raise it.
|
|
9
|
+
GOOGLE_BIGQUERY_MAX_BYTES_BILLED=1073741824
|
|
10
|
+
# GOOGLE_BIGQUERY_TOKEN_PATH=/absolute/private/directory/token.json
|
|
11
|
+
# GOOGLE_BIGQUERY_TOKEN_DIR=/absolute/private/directory/accounts
|
|
12
|
+
# GOOGLE_BIGQUERY_OAUTH_PORT=0
|
|
13
|
+
# GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/service-account.json
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
jobs:
|
|
6
|
+
test-and-build:
|
|
7
|
+
strategy:
|
|
8
|
+
fail-fast: false
|
|
9
|
+
matrix:
|
|
10
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
11
|
+
python: ['3.10', '3.12']
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: astral-sh/setup-uv@v7
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python }}
|
|
18
|
+
- run: uv sync --locked --extra dev
|
|
19
|
+
- run: uv run pytest
|
|
20
|
+
- run: uv run ruff check src tests scripts mcpb
|
|
21
|
+
- run: uv run ruff format --check src tests scripts mcpb
|
|
22
|
+
- run: uv build
|
|
23
|
+
- run: uv run python scripts/check_release.py
|
|
24
|
+
- run: uv run python scripts/build_mcpb.py
|
|
25
|
+
if: matrix.os == 'macos-latest' && matrix.python == '3.12'
|
|
26
|
+
- run: uv run python scripts/check_mcpb.py dist/mcpb
|
|
27
|
+
if: matrix.os == 'macos-latest' && matrix.python == '3.12'
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ['v*']
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: macos-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
- uses: astral-sh/setup-uv@v7
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.12'
|
|
16
|
+
- run: uv sync --locked --extra dev
|
|
17
|
+
- run: uv run pytest
|
|
18
|
+
- run: uv run ruff check src tests scripts mcpb
|
|
19
|
+
- run: uv run ruff format --check src tests scripts mcpb
|
|
20
|
+
- run: uv run python scripts/check_release.py
|
|
21
|
+
- run: uv build --out-dir dist/python
|
|
22
|
+
- name: Build and validate Mac extension
|
|
23
|
+
shell: bash
|
|
24
|
+
run: |
|
|
25
|
+
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
|
|
26
|
+
uv run python scripts/build_mcpb.py --tag "$GITHUB_REF_NAME"
|
|
27
|
+
else
|
|
28
|
+
uv run python scripts/build_mcpb.py
|
|
29
|
+
fi
|
|
30
|
+
- run: uv run python scripts/check_mcpb.py dist/mcpb
|
|
31
|
+
- uses: actions/upload-artifact@v6
|
|
32
|
+
with:
|
|
33
|
+
name: python-dist
|
|
34
|
+
path: dist/python/*
|
|
35
|
+
if-no-files-found: error
|
|
36
|
+
- uses: actions/upload-artifact@v6
|
|
37
|
+
with:
|
|
38
|
+
name: mac-extension
|
|
39
|
+
path: dist/mcpb/*
|
|
40
|
+
if-no-files-found: error
|
|
41
|
+
publish-pypi:
|
|
42
|
+
needs: build
|
|
43
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment:
|
|
46
|
+
name: pypi
|
|
47
|
+
url: https://pypi.org/p/flin-bigquery-mcp
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@v8
|
|
52
|
+
with:
|
|
53
|
+
name: python-dist
|
|
54
|
+
path: dist
|
|
55
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
publish-github:
|
|
57
|
+
needs: [build, publish-pypi]
|
|
58
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
permissions:
|
|
61
|
+
contents: write
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/download-artifact@v8
|
|
64
|
+
with:
|
|
65
|
+
name: python-dist
|
|
66
|
+
path: release-assets
|
|
67
|
+
- uses: actions/download-artifact@v8
|
|
68
|
+
with:
|
|
69
|
+
name: mac-extension
|
|
70
|
+
path: release-assets
|
|
71
|
+
- name: Publish release downloads
|
|
72
|
+
env:
|
|
73
|
+
GH_TOKEN: ${{ github.token }}
|
|
74
|
+
GH_REPO: ${{ github.repository }}
|
|
75
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
76
|
+
run: gh release create "$RELEASE_TAG" release-assets/* --verify-tag --title "$RELEASE_TAG" --generate-notes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flin GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flin-bigquery-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for exploring BigQuery and running bounded read-only SQL queries.
|
|
5
|
+
Project-URL: Homepage, https://github.com/flin-agency/flin-bigquery-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/flin-agency/flin-bigquery-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/flin-agency/flin-bigquery-mcp/issues
|
|
8
|
+
Author-email: flin GmbH <flin@flin.agency>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bigquery,claude,google-cloud,mcp
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: google-auth-oauthlib<2,>=1.2.0
|
|
14
|
+
Requires-Dist: google-auth<3,>=2.40.0
|
|
15
|
+
Requires-Dist: google-cloud-bigquery<4,>=3.30.0
|
|
16
|
+
Requires-Dist: mcp<2,>=1.6.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: build>=1.2.2; extra == 'dev'
|
|
19
|
+
Requires-Dist: jsonschema<5,>=4.23; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest<10,>=8; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.11; extra == 'dev'
|
|
22
|
+
Requires-Dist: tomli>=2; (python_version < '3.11') and extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# flin-bigquery-mcp
|
|
26
|
+
|
|
27
|
+
MCP server for exploring Google BigQuery and running bounded, read-only GoogleSQL queries. Follows the Python/FastMCP, desktop OAuth, multi-account token storage and `uv` packaging conventions of [flin Search Console MCP](https://github.com/flin-agency/flin-google-search-console-mcp) and [flin Keyword Planner MCP](https://github.com/flin-agency/flin-google-keyword-planner-mcp).
|
|
28
|
+
|
|
29
|
+
## Mac installation (Claude Desktop)
|
|
30
|
+
|
|
31
|
+
Download `flin-bigquery-mcp-0.1.0-macos.mcpb` from the [GitHub release](https://github.com/flin-agency/flin-bigquery-mcp/releases/latest) and open it with Claude Desktop. You can also use Claude Desktop's local extension installation dialog and select the file.
|
|
32
|
+
|
|
33
|
+
The installer asks for your Google Desktop OAuth client ID and secret, default billing project, optional BigQuery location, and maximum bytes billed per query (default 1 GiB). Configure Google Cloud as described below before the first data request. Google sign-in opens in the browser when needed.
|
|
34
|
+
|
|
35
|
+
The extension contains the server source and a dependency lock file. It uses the MCPB `uv` runtime with Python 3.12, supports Apple Silicon and Intel Macs, and needs internet access on first launch to install Python/dependencies. A current Claude Desktop version with `uv` extension support is required. The `.mcpb` is an unsigned local extension, not a notarized macOS application; it does not require administrator access or a separate repository checkout. For other MCP clients, use the PyPI or source configuration below.
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
1. Enable the **BigQuery API** in your Google Cloud project and configure billing for query jobs.
|
|
40
|
+
2. Grant the Google account **BigQuery Job User** (`roles/bigquery.jobUser`) on the job/billing project and **BigQuery Data Viewer** (`roles/bigquery.dataViewer`) on the datasets to read. Metadata discovery also depends on the account's project/dataset permissions. Avoid using an admin identity.
|
|
41
|
+
3. Configure the OAuth consent screen. For an external app in testing, add your Google account as a test user.
|
|
42
|
+
4. Create an OAuth client of type **Desktop app**. Use its client ID and secret below. The local loopback callback is managed by the server.
|
|
43
|
+
5. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and clone this repository. Add this entry to your MCP client's configuration:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"flin-bigquery-mcp": {
|
|
49
|
+
"command": "uv",
|
|
50
|
+
"args": [
|
|
51
|
+
"run", "--directory", "/absolute/path/to/flin-bigquery-mcp",
|
|
52
|
+
"--locked", "flin-bigquery-mcp"
|
|
53
|
+
],
|
|
54
|
+
"env": {
|
|
55
|
+
"GOOGLE_CLIENT_ID": "your-desktop-client-id",
|
|
56
|
+
"GOOGLE_CLIENT_SECRET": "your-desktop-client-secret",
|
|
57
|
+
"GOOGLE_BIGQUERY_PROJECT": "your-billing-project",
|
|
58
|
+
"GOOGLE_BIGQUERY_LOCATION": "EU"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Replace the path and values, then restart the client. `health_check` checks configuration locally. `list_projects` or the first data call opens Google sign-in when needed. Tokens are saved locally and refreshed automatically. OAuth waits for up to three minutes; retry the tool if sign-in expires.
|
|
66
|
+
|
|
67
|
+
This is a local **stdio** server, usable with clients such as Claude Desktop and Codex that support local MCP processes. No HTTP endpoint or cloud deployment is provisioned.
|
|
68
|
+
|
|
69
|
+
The published package can be used with `"command": "uvx", "args": ["flin-bigquery-mcp@latest"]`.
|
|
70
|
+
|
|
71
|
+
## Tools
|
|
72
|
+
|
|
73
|
+
| Tool | Purpose |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `health_check` | Local settings and saved-token status; does not verify Google access |
|
|
76
|
+
| `list_projects` | One page of accessible projects |
|
|
77
|
+
| `list_datasets` | One page of datasets in a project |
|
|
78
|
+
| `get_dataset` | Dataset location, description and labels |
|
|
79
|
+
| `list_tables` | One page of tables and views |
|
|
80
|
+
| `get_table` | Nested schema, row count, size, partitioning and clustering |
|
|
81
|
+
| `preview_table` | Bounded rows from a physical table without running a SQL query |
|
|
82
|
+
| `dry_run_query` | Validate a single SELECT and estimate scanned bytes |
|
|
83
|
+
| `run_query` | Validate and submit a bounded SELECT; returns a job reference |
|
|
84
|
+
| `get_query_results` | Poll a query job and page through its results |
|
|
85
|
+
|
|
86
|
+
All data tools support `account`. Project-specific tools use `project_id`, falling back to `GOOGLE_BIGQUERY_PROJECT`. Pass dataset and table IDs separately, without project prefixes. `list_projects` works without a default project. Query `project_id` determines where the job is created and billed; fully qualified SQL can read another accessible project.
|
|
87
|
+
|
|
88
|
+
### Example workflow
|
|
89
|
+
|
|
90
|
+
1. `health_check`, then `list_projects`.
|
|
91
|
+
2. `list_datasets`, `get_dataset` (check its location), `list_tables`, `get_table`.
|
|
92
|
+
3. `preview_table` to inspect a few rows, or `dry_run_query` to estimate a query.
|
|
93
|
+
4. `run_query` once, then `get_query_results` with the returned `job_id`, `project_id`, `location` and the same account.
|
|
94
|
+
5. If `job_complete` is false, poll again later. For subsequent pages, pass `next_page_token` unchanged. Do not call `run_query` again to poll.
|
|
95
|
+
|
|
96
|
+
A parameterized query:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"sql": "SELECT event_name, COUNT(*) AS events FROM `data-project.analytics.events_*` WHERE _TABLE_SUFFIX BETWEEN @start_date AND @end_date GROUP BY event_name ORDER BY events DESC LIMIT 20",
|
|
101
|
+
"project_id": "billing-project",
|
|
102
|
+
"location": "EU",
|
|
103
|
+
"parameters": [
|
|
104
|
+
{"name": "start_date", "type": "STRING", "value": "20261001"},
|
|
105
|
+
{"name": "end_date", "type": "STRING", "value": "20261007"}
|
|
106
|
+
],
|
|
107
|
+
"maximum_bytes_billed": 1073741824
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Named scalar parameters are supported; ARRAY/STRUCT parameters are not yet exposed. Result pages contain at most 1,000 rows (default 100); previews default to 20. A page token is tied to its resource/job and account. Results expire according to BigQuery's anonymous result-table retention. Dates/times use ISO strings, NUMERIC/BIGNUMERIC use decimal strings, BYTES use base64, and non-finite floats use strings. Arrays and nested records are preserved. Large STRING/BYTES cells can still produce large responses despite the row limit.
|
|
112
|
+
|
|
113
|
+
## Query controls and permissions
|
|
114
|
+
|
|
115
|
+
- Every execution is preceded by a Google BigQuery dry run. Google's parsed `statement_type` must be `SELECT`. Scripts, stored procedure calls, DDL, DML, exports and unknown statement types are rejected before execution.
|
|
116
|
+
- Default hard ceiling: **1 GiB (1,073,741,824 bytes) per query**. Set `GOOGLE_BIGQUERY_MAX_BYTES_BILLED` to change it. Tool arguments may lower but cannot raise this ceiling.
|
|
117
|
+
- Both the dry-run estimate and the submitted job are checked/bounded. Estimates may be incomplete for external or federated sources. `maximum_bytes_billed` limits BigQuery on-demand bytes, not every possible external, remote-function, ML or reservation charge.
|
|
118
|
+
- A SQL `LIMIT` limits output rows, not scanned bytes. Filter partitions and select only needed columns.
|
|
119
|
+
- `run_query` is marked non-idempotent and not read-only in MCP annotations because it creates a potentially billable job. Its allowed SQL only reads data. Polling results does not submit new queries.
|
|
120
|
+
- The Google client uses `jobs.insert`, which requires the **`https://www.googleapis.com/auth/bigquery`** scope. That scope is broader than the server's tool surface. IAM permissions are the underlying access boundary; the server deliberately exposes no write operations or user-supplied destination/job configuration.
|
|
121
|
+
- SELECT queries can invoke existing views, routines or remote functions allowed by IAM. SQL classification does not sandbox their downstream effects. Use a dedicated identity with only the intended dataset/job permissions.
|
|
122
|
+
- Query submission uses a generated job ID and disables automatic job re-execution. If a transport error occurs after submission, check BigQuery job history before manually retrying to avoid duplicate charges.
|
|
123
|
+
|
|
124
|
+
## Authentication and settings
|
|
125
|
+
|
|
126
|
+
| Variable | Default / use |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `GOOGLE_BIGQUERY_AUTH_MODE` | `oauth`; alternatively `adc` |
|
|
129
|
+
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Required in OAuth mode |
|
|
130
|
+
| `GOOGLE_BIGQUERY_PROJECT` | Default metadata and billing/job project; otherwise pass `project_id` |
|
|
131
|
+
| `GOOGLE_BIGQUERY_LOCATION` | Optional location (`EU`, `US`, or a region); must match the queried data |
|
|
132
|
+
| `GOOGLE_BIGQUERY_MAX_BYTES_BILLED` | `1073741824`; positive integer |
|
|
133
|
+
| `GOOGLE_BIGQUERY_TOKEN_PATH` | Default account's token file |
|
|
134
|
+
| `GOOGLE_BIGQUERY_TOKEN_DIR` | Named account token directory; defaults to the default token's directory |
|
|
135
|
+
| `GOOGLE_BIGQUERY_OAUTH_PORT` | `0` (available local loopback port) |
|
|
136
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google ADC credential file setting |
|
|
137
|
+
|
|
138
|
+
Default OAuth token paths:
|
|
139
|
+
|
|
140
|
+
- macOS: `~/Library/Application Support/flin-bigquery-mcp/token.json`
|
|
141
|
+
- Linux: `~/.config/flin-bigquery-mcp/token.json`
|
|
142
|
+
- Windows: `%APPDATA%\flin-bigquery-mcp\token.json`
|
|
143
|
+
|
|
144
|
+
Use `account: "work"` to keep another account in `work.json`. Account names allow letters, digits, `_`, `-`, `.` and cannot alias the default token file. Tokens are written atomically, with private file permissions on POSIX. Use a private directory owned by your user. Named accounts refer to separate sign-ins, not Google account IDs.
|
|
145
|
+
|
|
146
|
+
For service accounts, workload identity, or existing `gcloud auth application-default login` credentials:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"GOOGLE_BIGQUERY_AUTH_MODE": "adc",
|
|
151
|
+
"GOOGLE_BIGQUERY_PROJECT": "your-billing-project",
|
|
152
|
+
"GOOGLE_BIGQUERY_LOCATION": "EU"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Optionally set `GOOGLE_APPLICATION_CREDENTIALS` to a credential file outside the repository. ADC mode does not support named `account` values. `health_check` always returns `adc_not_checked` with `ok: false` in ADC mode; verify access through a data tool. It never contacts metadata servers or opens a browser to probe ADC. Grant the ADC identity the same minimal BigQuery roles.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
uv sync --locked --extra dev
|
|
162
|
+
uv run pytest
|
|
163
|
+
uv run ruff check src tests scripts mcpb
|
|
164
|
+
uv run ruff format --check src tests scripts mcpb
|
|
165
|
+
uv build
|
|
166
|
+
uv run python scripts/build_mcpb.py
|
|
167
|
+
uv run python scripts/check_mcpb.py dist/mcpb
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`.env.example` documents configuration. The server does not implicitly load `.env`; use client `env` settings or `uv run --env-file .env flin-bigquery-mcp` for local development. Never commit credentials or tokens.
|
|
171
|
+
|
|
172
|
+
Tests run without credentials and mock Google API calls. They cover OAuth refresh/storage, account isolation, query rejection, byte ceilings, pagination, serialization, sanitized errors, and a real stdio MCP handshake/tool call. See [testing](docs/testing.md) for the optional live check.
|
|
173
|
+
|
|
174
|
+
See [release instructions](docs/release.md), [privacy](PRIVACY.md), and [support](SUPPORT.md).
|
|
175
|
+
|
|
176
|
+
## Troubleshooting
|
|
177
|
+
|
|
178
|
+
- `missing_configuration` / `invalid_configuration`: check the selected authentication mode, required OAuth values, project, account name and positive byte ceiling.
|
|
179
|
+
- `authentication_required`: complete Google sign-in or configure ADC. External OAuth apps in testing may require renewed consent.
|
|
180
|
+
- `permission_denied`: check API enablement, job-user and data-viewer access, billing and quotas.
|
|
181
|
+
- `not_found`: check IDs and the returned job location. An EU job cannot be fetched as a US job.
|
|
182
|
+
- `query_rejected`: use a single SELECT (including CTEs), not a script or write operation.
|
|
183
|
+
- `cost_limit_exceeded`: reduce scanned columns/date partitions or deliberately change the configured server ceiling.
|
|
184
|
+
- `invalid_request`: check SQL, parameter types and region. Detailed SQL-bearing upstream errors are deliberately not returned to the model; inspect the Google Cloud console when needed.
|
|
185
|
+
|
|
186
|
+
## Official references
|
|
187
|
+
|
|
188
|
+
- [BigQuery access control](https://cloud.google.com/bigquery/docs/access-control)
|
|
189
|
+
- [Running queries and dry runs](https://cloud.google.com/bigquery/docs/running-queries)
|
|
190
|
+
- [Job insertion and OAuth scopes](https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert)
|
|
191
|
+
- [Query job configuration and maximum bytes billed](https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJobConfig)
|
|
192
|
+
- [Desktop OAuth](https://developers.google.com/identity/protocols/oauth2/native-app)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Privacy
|
|
2
|
+
|
|
3
|
+
This local server sends selected project, dataset, table and query requests to Google BigQuery. Results are returned to the MCP client, whose own data handling policy applies. No flin-hosted service, analytics or telemetry is used by this package.
|
|
4
|
+
|
|
5
|
+
OAuth credentials are stored on the user's computer and refreshed through Google. ADC uses the credential source configured in the environment. The local OAuth token files contain sensitive refresh tokens and must not be shared or committed. To disconnect an OAuth account, stop the server, remove its local token file and revoke the application's Google account authorization if needed.
|
|
6
|
+
|
|
7
|
+
Query SQL and job metadata can be retained in Google Cloud job history and audit logs under the project's policies. Source data and table descriptions are returned only when requested by a tool. Remote functions or federated queries may involve additional services configured in Google Cloud.
|
|
8
|
+
|
|
9
|
+
See [Google's privacy policy](https://policies.google.com/privacy).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# flin-bigquery-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for exploring Google BigQuery and running bounded, read-only GoogleSQL queries. Follows the Python/FastMCP, desktop OAuth, multi-account token storage and `uv` packaging conventions of [flin Search Console MCP](https://github.com/flin-agency/flin-google-search-console-mcp) and [flin Keyword Planner MCP](https://github.com/flin-agency/flin-google-keyword-planner-mcp).
|
|
4
|
+
|
|
5
|
+
## Mac installation (Claude Desktop)
|
|
6
|
+
|
|
7
|
+
Download `flin-bigquery-mcp-0.1.0-macos.mcpb` from the [GitHub release](https://github.com/flin-agency/flin-bigquery-mcp/releases/latest) and open it with Claude Desktop. You can also use Claude Desktop's local extension installation dialog and select the file.
|
|
8
|
+
|
|
9
|
+
The installer asks for your Google Desktop OAuth client ID and secret, default billing project, optional BigQuery location, and maximum bytes billed per query (default 1 GiB). Configure Google Cloud as described below before the first data request. Google sign-in opens in the browser when needed.
|
|
10
|
+
|
|
11
|
+
The extension contains the server source and a dependency lock file. It uses the MCPB `uv` runtime with Python 3.12, supports Apple Silicon and Intel Macs, and needs internet access on first launch to install Python/dependencies. A current Claude Desktop version with `uv` extension support is required. The `.mcpb` is an unsigned local extension, not a notarized macOS application; it does not require administrator access or a separate repository checkout. For other MCP clients, use the PyPI or source configuration below.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
1. Enable the **BigQuery API** in your Google Cloud project and configure billing for query jobs.
|
|
16
|
+
2. Grant the Google account **BigQuery Job User** (`roles/bigquery.jobUser`) on the job/billing project and **BigQuery Data Viewer** (`roles/bigquery.dataViewer`) on the datasets to read. Metadata discovery also depends on the account's project/dataset permissions. Avoid using an admin identity.
|
|
17
|
+
3. Configure the OAuth consent screen. For an external app in testing, add your Google account as a test user.
|
|
18
|
+
4. Create an OAuth client of type **Desktop app**. Use its client ID and secret below. The local loopback callback is managed by the server.
|
|
19
|
+
5. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and clone this repository. Add this entry to your MCP client's configuration:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"flin-bigquery-mcp": {
|
|
25
|
+
"command": "uv",
|
|
26
|
+
"args": [
|
|
27
|
+
"run", "--directory", "/absolute/path/to/flin-bigquery-mcp",
|
|
28
|
+
"--locked", "flin-bigquery-mcp"
|
|
29
|
+
],
|
|
30
|
+
"env": {
|
|
31
|
+
"GOOGLE_CLIENT_ID": "your-desktop-client-id",
|
|
32
|
+
"GOOGLE_CLIENT_SECRET": "your-desktop-client-secret",
|
|
33
|
+
"GOOGLE_BIGQUERY_PROJECT": "your-billing-project",
|
|
34
|
+
"GOOGLE_BIGQUERY_LOCATION": "EU"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Replace the path and values, then restart the client. `health_check` checks configuration locally. `list_projects` or the first data call opens Google sign-in when needed. Tokens are saved locally and refreshed automatically. OAuth waits for up to three minutes; retry the tool if sign-in expires.
|
|
42
|
+
|
|
43
|
+
This is a local **stdio** server, usable with clients such as Claude Desktop and Codex that support local MCP processes. No HTTP endpoint or cloud deployment is provisioned.
|
|
44
|
+
|
|
45
|
+
The published package can be used with `"command": "uvx", "args": ["flin-bigquery-mcp@latest"]`.
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
| Tool | Purpose |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `health_check` | Local settings and saved-token status; does not verify Google access |
|
|
52
|
+
| `list_projects` | One page of accessible projects |
|
|
53
|
+
| `list_datasets` | One page of datasets in a project |
|
|
54
|
+
| `get_dataset` | Dataset location, description and labels |
|
|
55
|
+
| `list_tables` | One page of tables and views |
|
|
56
|
+
| `get_table` | Nested schema, row count, size, partitioning and clustering |
|
|
57
|
+
| `preview_table` | Bounded rows from a physical table without running a SQL query |
|
|
58
|
+
| `dry_run_query` | Validate a single SELECT and estimate scanned bytes |
|
|
59
|
+
| `run_query` | Validate and submit a bounded SELECT; returns a job reference |
|
|
60
|
+
| `get_query_results` | Poll a query job and page through its results |
|
|
61
|
+
|
|
62
|
+
All data tools support `account`. Project-specific tools use `project_id`, falling back to `GOOGLE_BIGQUERY_PROJECT`. Pass dataset and table IDs separately, without project prefixes. `list_projects` works without a default project. Query `project_id` determines where the job is created and billed; fully qualified SQL can read another accessible project.
|
|
63
|
+
|
|
64
|
+
### Example workflow
|
|
65
|
+
|
|
66
|
+
1. `health_check`, then `list_projects`.
|
|
67
|
+
2. `list_datasets`, `get_dataset` (check its location), `list_tables`, `get_table`.
|
|
68
|
+
3. `preview_table` to inspect a few rows, or `dry_run_query` to estimate a query.
|
|
69
|
+
4. `run_query` once, then `get_query_results` with the returned `job_id`, `project_id`, `location` and the same account.
|
|
70
|
+
5. If `job_complete` is false, poll again later. For subsequent pages, pass `next_page_token` unchanged. Do not call `run_query` again to poll.
|
|
71
|
+
|
|
72
|
+
A parameterized query:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"sql": "SELECT event_name, COUNT(*) AS events FROM `data-project.analytics.events_*` WHERE _TABLE_SUFFIX BETWEEN @start_date AND @end_date GROUP BY event_name ORDER BY events DESC LIMIT 20",
|
|
77
|
+
"project_id": "billing-project",
|
|
78
|
+
"location": "EU",
|
|
79
|
+
"parameters": [
|
|
80
|
+
{"name": "start_date", "type": "STRING", "value": "20261001"},
|
|
81
|
+
{"name": "end_date", "type": "STRING", "value": "20261007"}
|
|
82
|
+
],
|
|
83
|
+
"maximum_bytes_billed": 1073741824
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Named scalar parameters are supported; ARRAY/STRUCT parameters are not yet exposed. Result pages contain at most 1,000 rows (default 100); previews default to 20. A page token is tied to its resource/job and account. Results expire according to BigQuery's anonymous result-table retention. Dates/times use ISO strings, NUMERIC/BIGNUMERIC use decimal strings, BYTES use base64, and non-finite floats use strings. Arrays and nested records are preserved. Large STRING/BYTES cells can still produce large responses despite the row limit.
|
|
88
|
+
|
|
89
|
+
## Query controls and permissions
|
|
90
|
+
|
|
91
|
+
- Every execution is preceded by a Google BigQuery dry run. Google's parsed `statement_type` must be `SELECT`. Scripts, stored procedure calls, DDL, DML, exports and unknown statement types are rejected before execution.
|
|
92
|
+
- Default hard ceiling: **1 GiB (1,073,741,824 bytes) per query**. Set `GOOGLE_BIGQUERY_MAX_BYTES_BILLED` to change it. Tool arguments may lower but cannot raise this ceiling.
|
|
93
|
+
- Both the dry-run estimate and the submitted job are checked/bounded. Estimates may be incomplete for external or federated sources. `maximum_bytes_billed` limits BigQuery on-demand bytes, not every possible external, remote-function, ML or reservation charge.
|
|
94
|
+
- A SQL `LIMIT` limits output rows, not scanned bytes. Filter partitions and select only needed columns.
|
|
95
|
+
- `run_query` is marked non-idempotent and not read-only in MCP annotations because it creates a potentially billable job. Its allowed SQL only reads data. Polling results does not submit new queries.
|
|
96
|
+
- The Google client uses `jobs.insert`, which requires the **`https://www.googleapis.com/auth/bigquery`** scope. That scope is broader than the server's tool surface. IAM permissions are the underlying access boundary; the server deliberately exposes no write operations or user-supplied destination/job configuration.
|
|
97
|
+
- SELECT queries can invoke existing views, routines or remote functions allowed by IAM. SQL classification does not sandbox their downstream effects. Use a dedicated identity with only the intended dataset/job permissions.
|
|
98
|
+
- Query submission uses a generated job ID and disables automatic job re-execution. If a transport error occurs after submission, check BigQuery job history before manually retrying to avoid duplicate charges.
|
|
99
|
+
|
|
100
|
+
## Authentication and settings
|
|
101
|
+
|
|
102
|
+
| Variable | Default / use |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `GOOGLE_BIGQUERY_AUTH_MODE` | `oauth`; alternatively `adc` |
|
|
105
|
+
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Required in OAuth mode |
|
|
106
|
+
| `GOOGLE_BIGQUERY_PROJECT` | Default metadata and billing/job project; otherwise pass `project_id` |
|
|
107
|
+
| `GOOGLE_BIGQUERY_LOCATION` | Optional location (`EU`, `US`, or a region); must match the queried data |
|
|
108
|
+
| `GOOGLE_BIGQUERY_MAX_BYTES_BILLED` | `1073741824`; positive integer |
|
|
109
|
+
| `GOOGLE_BIGQUERY_TOKEN_PATH` | Default account's token file |
|
|
110
|
+
| `GOOGLE_BIGQUERY_TOKEN_DIR` | Named account token directory; defaults to the default token's directory |
|
|
111
|
+
| `GOOGLE_BIGQUERY_OAUTH_PORT` | `0` (available local loopback port) |
|
|
112
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google ADC credential file setting |
|
|
113
|
+
|
|
114
|
+
Default OAuth token paths:
|
|
115
|
+
|
|
116
|
+
- macOS: `~/Library/Application Support/flin-bigquery-mcp/token.json`
|
|
117
|
+
- Linux: `~/.config/flin-bigquery-mcp/token.json`
|
|
118
|
+
- Windows: `%APPDATA%\flin-bigquery-mcp\token.json`
|
|
119
|
+
|
|
120
|
+
Use `account: "work"` to keep another account in `work.json`. Account names allow letters, digits, `_`, `-`, `.` and cannot alias the default token file. Tokens are written atomically, with private file permissions on POSIX. Use a private directory owned by your user. Named accounts refer to separate sign-ins, not Google account IDs.
|
|
121
|
+
|
|
122
|
+
For service accounts, workload identity, or existing `gcloud auth application-default login` credentials:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"GOOGLE_BIGQUERY_AUTH_MODE": "adc",
|
|
127
|
+
"GOOGLE_BIGQUERY_PROJECT": "your-billing-project",
|
|
128
|
+
"GOOGLE_BIGQUERY_LOCATION": "EU"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Optionally set `GOOGLE_APPLICATION_CREDENTIALS` to a credential file outside the repository. ADC mode does not support named `account` values. `health_check` always returns `adc_not_checked` with `ok: false` in ADC mode; verify access through a data tool. It never contacts metadata servers or opens a browser to probe ADC. Grant the ADC identity the same minimal BigQuery roles.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
uv sync --locked --extra dev
|
|
138
|
+
uv run pytest
|
|
139
|
+
uv run ruff check src tests scripts mcpb
|
|
140
|
+
uv run ruff format --check src tests scripts mcpb
|
|
141
|
+
uv build
|
|
142
|
+
uv run python scripts/build_mcpb.py
|
|
143
|
+
uv run python scripts/check_mcpb.py dist/mcpb
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`.env.example` documents configuration. The server does not implicitly load `.env`; use client `env` settings or `uv run --env-file .env flin-bigquery-mcp` for local development. Never commit credentials or tokens.
|
|
147
|
+
|
|
148
|
+
Tests run without credentials and mock Google API calls. They cover OAuth refresh/storage, account isolation, query rejection, byte ceilings, pagination, serialization, sanitized errors, and a real stdio MCP handshake/tool call. See [testing](docs/testing.md) for the optional live check.
|
|
149
|
+
|
|
150
|
+
See [release instructions](docs/release.md), [privacy](PRIVACY.md), and [support](SUPPORT.md).
|
|
151
|
+
|
|
152
|
+
## Troubleshooting
|
|
153
|
+
|
|
154
|
+
- `missing_configuration` / `invalid_configuration`: check the selected authentication mode, required OAuth values, project, account name and positive byte ceiling.
|
|
155
|
+
- `authentication_required`: complete Google sign-in or configure ADC. External OAuth apps in testing may require renewed consent.
|
|
156
|
+
- `permission_denied`: check API enablement, job-user and data-viewer access, billing and quotas.
|
|
157
|
+
- `not_found`: check IDs and the returned job location. An EU job cannot be fetched as a US job.
|
|
158
|
+
- `query_rejected`: use a single SELECT (including CTEs), not a script or write operation.
|
|
159
|
+
- `cost_limit_exceeded`: reduce scanned columns/date partitions or deliberately change the configured server ceiling.
|
|
160
|
+
- `invalid_request`: check SQL, parameter types and region. Detailed SQL-bearing upstream errors are deliberately not returned to the model; inspect the Google Cloud console when needed.
|
|
161
|
+
|
|
162
|
+
## Official references
|
|
163
|
+
|
|
164
|
+
- [BigQuery access control](https://cloud.google.com/bigquery/docs/access-control)
|
|
165
|
+
- [Running queries and dry runs](https://cloud.google.com/bigquery/docs/running-queries)
|
|
166
|
+
- [Job insertion and OAuth scopes](https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert)
|
|
167
|
+
- [Query job configuration and maximum bytes billed](https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJobConfig)
|
|
168
|
+
- [Desktop OAuth](https://developers.google.com/identity/protocols/oauth2/native-app)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Support
|
|
2
|
+
|
|
3
|
+
Report issues at https://github.com/flin-agency/flin-bigquery-mcp/issues or contact flin@flin.agency.
|
|
4
|
+
|
|
5
|
+
Include the package version, operating system, MCP client and sanitized error code. Never include OAuth secrets, token files, service-account keys, private data or sensitive SQL.
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-label="flin BigQuery">
|
|
2
|
+
<rect width="256" height="256" rx="52" fill="#142D35"/>
|
|
3
|
+
<path d="M57 185V142H87V185ZM113 185V103H143V185ZM169 185V64H199V185Z" fill="#A8EDC6"/>
|
|
4
|
+
<path d="M57 111L119 72L156 85L199 42" fill="none" stroke="#FFFFFF" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Release
|
|
2
|
+
|
|
3
|
+
The project follows the flin GitHub Actions / PyPI Trusted Publisher release model. No release is published merely by pushing source to main.
|
|
4
|
+
|
|
5
|
+
1. Configure a PyPI Trusted Publisher: owner `flin-agency`, repository `flin-bigquery-mcp`, workflow `release.yml`, environment `pypi`.
|
|
6
|
+
2. Update the version in `pyproject.toml` and `src/flin_bigquery_mcp/__init__.py`; run `uv lock`.
|
|
7
|
+
3. Run tests, lint, formatting and `uv build`. Run `uv run python scripts/build_mcpb.py`, and `uv run python scripts/check_mcpb.py dist/mcpb`. Verify the generated wheel and source distribution contain the runtime and no credentials.
|
|
8
|
+
4. Commit and push the tested sources, then create and push the matching `vX.Y.Z` tag when publication is intended.
|
|
9
|
+
5. The workflow verifies the tag, runs checks, smoke-tests the Mac extension on macOS, builds distributions and publishes through PyPI's trusted publisher. Confirm package installation from PyPI before recommending `uvx flin-bigquery-mcp@latest`.
|
|
10
|
+
|
|
11
|
+
Manual workflow dispatch builds artifacts only; publication requires a version tag. Configure GitHub's `pypi` environment protection as appropriate for the organization.
|
|
12
|
+
|
|
13
|
+
After PyPI publication succeeds, the workflow creates a GitHub release with the Python distributions, the macOS `.mcpb` extension and `SHA256SUMS`. The extension is staged from an explicit runtime allowlist; development files, credentials and local environments are excluded.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Run `uv sync --locked --extra dev`, then `uv run pytest`. Tests never require Google credentials or execute billable queries. The stdio test launches the actual server, initializes an MCP session, discovers tools, checks annotations, calls health_check and verifies input validation.
|
|
4
|
+
|
|
5
|
+
Optional live validation after configuring a dedicated test identity:
|
|
6
|
+
|
|
7
|
+
1. Run `health_check`, then `list_projects` and sign in.
|
|
8
|
+
2. List a known project's datasets and inspect one dataset/table.
|
|
9
|
+
3. Preview one physical table with `page_size: 1`; continue using its token.
|
|
10
|
+
4. Dry-run `SELECT @value AS value` with an INT64 parameter of 1.
|
|
11
|
+
5. Submit the same query once with `run_query`; retrieve its result using the returned job ID and location.
|
|
12
|
+
6. Query `SELECT x FROM UNNEST([1,2,3]) AS x ORDER BY x`; retrieve three pages with `page_size: 1`, checking no rows repeat.
|
|
13
|
+
7. Request `maximum_bytes_billed` above the configured ceiling; it must be rejected before any API query is sent.
|
|
14
|
+
|
|
15
|
+
Do not run write statements against production to test rejection. Automated tests cover Google's non-SELECT classifications with mocked responses. Live testing is separate from the offline suite and may incur Google charges.
|