data-platform-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.
Files changed (38) hide show
  1. data_platform_mcp-0.1.0/LICENSE +21 -0
  2. data_platform_mcp-0.1.0/PKG-INFO +421 -0
  3. data_platform_mcp-0.1.0/README.md +392 -0
  4. data_platform_mcp-0.1.0/pyproject.toml +53 -0
  5. data_platform_mcp-0.1.0/setup.cfg +4 -0
  6. data_platform_mcp-0.1.0/src/data_platform_mcp/__init__.py +12 -0
  7. data_platform_mcp-0.1.0/src/data_platform_mcp/__main__.py +4 -0
  8. data_platform_mcp-0.1.0/src/data_platform_mcp/clients.py +74 -0
  9. data_platform_mcp-0.1.0/src/data_platform_mcp/config.py +443 -0
  10. data_platform_mcp-0.1.0/src/data_platform_mcp/diagnostics.py +328 -0
  11. data_platform_mcp-0.1.0/src/data_platform_mcp/errors.py +96 -0
  12. data_platform_mcp-0.1.0/src/data_platform_mcp/formatting.py +42 -0
  13. data_platform_mcp-0.1.0/src/data_platform_mcp/observability.py +232 -0
  14. data_platform_mcp-0.1.0/src/data_platform_mcp/provisioning.py +44 -0
  15. data_platform_mcp-0.1.0/src/data_platform_mcp/registration.py +35 -0
  16. data_platform_mcp-0.1.0/src/data_platform_mcp/scripts/setup-service-account.sh +168 -0
  17. data_platform_mcp-0.1.0/src/data_platform_mcp/server.py +154 -0
  18. data_platform_mcp-0.1.0/src/data_platform_mcp/tools/__init__.py +1 -0
  19. data_platform_mcp-0.1.0/src/data_platform_mcp/tools/discovery_tools.py +310 -0
  20. data_platform_mcp-0.1.0/src/data_platform_mcp/tools/environment_tools.py +43 -0
  21. data_platform_mcp-0.1.0/src/data_platform_mcp/tools/query_tools.py +182 -0
  22. data_platform_mcp-0.1.0/src/data_platform_mcp.egg-info/PKG-INFO +421 -0
  23. data_platform_mcp-0.1.0/src/data_platform_mcp.egg-info/SOURCES.txt +36 -0
  24. data_platform_mcp-0.1.0/src/data_platform_mcp.egg-info/dependency_links.txt +1 -0
  25. data_platform_mcp-0.1.0/src/data_platform_mcp.egg-info/entry_points.txt +2 -0
  26. data_platform_mcp-0.1.0/src/data_platform_mcp.egg-info/requires.txt +7 -0
  27. data_platform_mcp-0.1.0/src/data_platform_mcp.egg-info/top_level.txt +1 -0
  28. data_platform_mcp-0.1.0/tests/test_config.py +249 -0
  29. data_platform_mcp-0.1.0/tests/test_diagnostics.py +226 -0
  30. data_platform_mcp-0.1.0/tests/test_environments.py +213 -0
  31. data_platform_mcp-0.1.0/tests/test_errors.py +60 -0
  32. data_platform_mcp-0.1.0/tests/test_eval_scoring.py +260 -0
  33. data_platform_mcp-0.1.0/tests/test_formatting.py +65 -0
  34. data_platform_mcp-0.1.0/tests/test_observability.py +178 -0
  35. data_platform_mcp-0.1.0/tests/test_packaging.py +82 -0
  36. data_platform_mcp-0.1.0/tests/test_payload_shape.py +181 -0
  37. data_platform_mcp-0.1.0/tests/test_protocol.py +122 -0
  38. data_platform_mcp-0.1.0/tests/test_query_guard.py +176 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dimuthu
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,421 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-platform-mcp
3
+ Version: 0.1.0
4
+ Summary: Read-only BigQuery MCP server for asking data questions in plain language
5
+ Author: Dimuthu Bhathiya
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/deBilla/bigquery-mcp
8
+ Project-URL: Repository, https://github.com/deBilla/bigquery-mcp
9
+ Project-URL: Issues, https://github.com/deBilla/bigquery-mcp/issues
10
+ Keywords: mcp,model-context-protocol,bigquery,gcp,google-cloud,sql,analytics
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp<2,>=1.2.0
23
+ Requires-Dist: google-cloud-bigquery>=3.25.0
24
+ Requires-Dist: google-auth>=2.28.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: anyio>=4.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # BigQuery MCP
31
+
32
+ [![CI](https://github.com/deBilla/bigquery-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/deBilla/bigquery-mcp/actions/workflows/ci.yml)
33
+
34
+ A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) server over
35
+ Google BigQuery. It lets an AI client (Claude Code, Claude Desktop, …) answer
36
+ plain-language data questions by discovering schema and running `SELECT` queries.
37
+
38
+ The AI does the natural-language → SQL translation; this server just safely
39
+ executes against BigQuery under **your own** Google credentials.
40
+
41
+ - **Repo:** https://github.com/deBilla/bigquery-mcp
42
+ - **Clone (SSH):** `git@github.com:deBilla/bigquery-mcp.git`
43
+
44
+ ---
45
+
46
+ ## Tools exposed
47
+
48
+ | Tool | Purpose | Cost |
49
+ |------|---------|------|
50
+ | `list_datasets` | List datasets in the project | free |
51
+ | `list_tables` | List tables/views in a dataset | free |
52
+ | `get_table_schema` | Columns (nested paths expanded), **partitioning**, size, row count | free |
53
+ | `check_table_freshness` | When each table was last written — catches stale sources | free |
54
+ | `list_environments` | Which BigQuery environments are configured, and the default | free |
55
+ | `run_query` | Run a validated, read-only `SELECT` and return rows | scans data |
56
+
57
+ Only `run_query` costs anything, so the discovery tools are the ones to spend
58
+ first. Two of them exist to prevent specific, repeated mistakes:
59
+
60
+ - **`get_table_schema` reports partitioning from table metadata, never from
61
+ column names.** A table with a `partition_date` column may not be partitioned
62
+ — in which case no `WHERE` clause reduces the scan and every query reads the
63
+ whole table. The response flags this explicitly when the table is large.
64
+ - **`check_table_freshness` finds tables that stopped being written to** without
65
+ being dropped. Those return stale data rather than an error, which is the
66
+ failure mode nobody notices.
67
+
68
+ ---
69
+
70
+ ## Environments
71
+
72
+ One server answers questions about several targets — a warehouse and its
73
+ staging copy, or two regions of the same project. Every tool takes an optional
74
+ `environment`; omitting it uses the default.
75
+
76
+ ```toml
77
+ # ~/.config/data-platform-mcp/config.toml
78
+ default_environment = "warehouse"
79
+
80
+ [environments.warehouse]
81
+ project = "my-data-platform"
82
+ impersonate = "data-platform-mcp-ro@my-data-platform.iam.gserviceaccount.com"
83
+ dataset_allowlist = ["sales", "events"]
84
+
85
+ [environments.central] # same project, different region
86
+ project = "my-data-platform"
87
+ location = "us-central1"
88
+ ```
89
+
90
+ See [`config.toml.example`](config.toml.example) for every setting, or set
91
+ `BQ_MCP_ENVIRONMENTS` to the same structure as JSON. **A single `BQ_PROJECT`
92
+ still works unchanged** — it becomes one environment named `default`.
93
+
94
+ An environment can be named by its own name, an alias, the built-in shorthands
95
+ (`prod`, `stg`, `dev`, `live`) or its project id. An **unknown** name is an
96
+ error naming the valid options, never a silent fall back to the default: a typo
97
+ that answered a production question from staging would be invisible in the
98
+ reply. Every result echoes back the environment it came from.
99
+
100
+ Regions are why this matters most here. BigQuery cannot query across locations,
101
+ and its error for trying names neither location, so it reads as a missing
102
+ table. One environment per location; `doctor` reports which datasets are where.
103
+
104
+ ---
105
+
106
+ ## Read-only as a property of the identity
107
+
108
+ The SELECT-only guard and the `readOnlyHint` annotations are promises about
109
+ this code. Pointing the server at a service account that holds only
110
+ `roles/bigquery.jobUser` and a dataset-scoped `roles/bigquery.dataViewer` makes
111
+ it a fact about the credentials — enforced by IAM whatever the code does, and
112
+ whatever your own roles allow:
113
+
114
+ ```bash
115
+ data-platform-mcp setup --project my-data-platform --datasets sales,events
116
+ ```
117
+
118
+ Creates the account, grants those two roles, and gives you
119
+ `roles/iam.serviceAccountTokenCreator` on it so the server can impersonate it.
120
+ Add `--dry-run` to see the commands first; it is safe to re-run.
121
+
122
+ With `--datasets`, the dataset allowlist stops being an `if` statement in this
123
+ process and becomes a grant Google enforces.
124
+
125
+ ---
126
+
127
+ ## Quick start (per user)
128
+
129
+ Each person runs their own local copy. Queries execute under **their own**
130
+ BigQuery/IAM permissions, so existing access controls decide who can see what.
131
+ You need Python 3.11+ and the `gcloud` CLI installed.
132
+
133
+ ### 1. Install
134
+
135
+ The package is published as **`data-platform-mcp`** (`bigquery-mcp` was already
136
+ taken on PyPI by an unrelated project). Once a release is tagged, no checkout is
137
+ needed — the client can fetch and run it directly:
138
+
139
+ ```bash
140
+ uvx data-platform-mcp --version
141
+ ```
142
+
143
+ > **Not yet published.** No version tag has been pushed, so use the source
144
+ > install below until one is. See [Releasing](#releasing).
145
+
146
+ **From source:**
147
+
148
+ ```bash
149
+ git clone git@github.com:deBilla/bigquery-mcp.git
150
+ cd bigquery-mcp
151
+
152
+ python3 -m venv .venv
153
+ ./.venv/bin/pip install -e .
154
+ ```
155
+
156
+ Either way you get a `data-platform-mcp` command, which is what the client runs.
157
+
158
+ ### 2. Authenticate to Google (one time)
159
+
160
+ Uses [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials).
161
+ Run this once; queries then execute as you.
162
+
163
+ ```bash
164
+ gcloud auth application-default login
165
+ ```
166
+
167
+ > Your account needs **BigQuery Data Viewer** + **BigQuery Job User** on the
168
+ > project you intend to query.
169
+
170
+ ### 3. Check your setup
171
+
172
+ ```bash
173
+ BQ_PROJECT=your-gcp-project data-platform-mcp doctor
174
+ ```
175
+
176
+ Checks credentials, job permission, dataset visibility and — the one that
177
+ catches people — **dataset regions**. BigQuery cannot query a dataset from a
178
+ different location, and its own error names neither the location it wanted nor
179
+ the one the dataset is in, so it reads as a missing table. `doctor` names both:
180
+
181
+ ```
182
+ [ ok ] run a query in my-project (location US)
183
+ [ ok ] 39 datasets visible (no allowlist; all are readable)
184
+ [ warn ] 6 of 39 datasets are outside location US
185
+ US-CENTRAL1: analytics_raw, business_data, ds_public, pg_public, public, recommendations
186
+ BigQuery cannot query these from US, and cannot join them with
187
+ datasets that are in it.
188
+ Fix: set BQ_LOCATION to the region you need, and run a separate
189
+ server for datasets in another one.
190
+ ```
191
+
192
+ A dataset in another region is a warning; one on your `BQ_DATASET_ALLOWLIST` is
193
+ a failure, because no tool call could ever read it.
194
+
195
+ ### 4. Register with your AI client
196
+
197
+ Replace `your-gcp-project` with your GCP project ID.
198
+
199
+ **Claude Code** — once published:
200
+
201
+ ```bash
202
+ claude mcp add bigquery \
203
+ --env BQ_PROJECT=your-gcp-project \
204
+ -- uvx data-platform-mcp
205
+ ```
206
+
207
+ From a source install, point at the checkout instead (replace
208
+ `/abs/path/bigquery-mcp`):
209
+
210
+ ```bash
211
+ claude mcp add bigquery \
212
+ --env BQ_PROJECT=your-gcp-project \
213
+ -- /abs/path/bigquery-mcp/.venv/bin/data-platform-mcp
214
+ ```
215
+
216
+ **Claude Desktop** — add to `claude_desktop_config.json`:
217
+
218
+ ```json
219
+ {
220
+ "mcpServers": {
221
+ "bigquery": {
222
+ "command": "/abs/path/bigquery-mcp/.venv/bin/data-platform-mcp",
223
+ "args": [],
224
+ "env": { "BQ_PROJECT": "your-gcp-project" }
225
+ }
226
+ }
227
+ }
228
+ ```
229
+
230
+ ### 5. Restart the client and ask a question
231
+
232
+ > "Which datasets are available? In the `sales` dataset, how many rows does the
233
+ > `orders` table have?"
234
+
235
+ ---
236
+
237
+ ## Safety
238
+
239
+ - Every query is **dry-run first** to validate it and estimate bytes scanned.
240
+ - **Only `SELECT` / `WITH`** statements run — no writes, DDL, or DML.
241
+ - **Cost confirmation:** a query estimated to scan more than `BQ_WARN_BYTES`
242
+ (default 1 GB) does **not** run. It returns `status: "confirmation_required"`
243
+ with the estimated scan size and dollar cost so the client can ask before
244
+ proceeding. Re-call with `confirm_expensive=true` to run it.
245
+ - **Hard cap:** queries above `BQ_MAX_BYTES_BILLED` (default 5 GB) never run,
246
+ even with confirmation — a runaway-cost backstop.
247
+ - Optional **dataset allowlist** restricts what can be read.
248
+ - **Refusals are protocol errors.** Anything the server declines to do — a
249
+ non-`SELECT` statement, a disallowed dataset, a query over the hard cap —
250
+ arrives with MCP's `isError` set, so it cannot be mistaken for a result.
251
+ `confirmation_required` is the deliberate exception: it is a normal result,
252
+ because the agent is meant to relay it and come back.
253
+ - **Responses are size-bounded.** `run_query` stops adding rows once the
254
+ serialised response reaches ~40k characters and sets `stopped_for_size`, so a
255
+ wide result cannot quietly consume the whole context window. A partial answer
256
+ always says that it is partial.
257
+ - **SQL is never written to the audit log** — only a hash and a length. Query
258
+ text routinely contains the user IDs or emails it filters on.
259
+
260
+ ### Cost-confirmation flow
261
+
262
+ ```
263
+ run_query(sql)
264
+ │ dry run estimates the scan
265
+ ├── ≤ 1 GB ........... runs, returns rows + estimated_cost_usd
266
+ ├── 1–5 GB .......... status: confirmation_required (size + $ estimate) → ask user
267
+ │ → run_query(sql, confirm_expensive=true) runs it
268
+ └── > 5 GB ........... rejected, never runs
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Configuration (environment variables)
274
+
275
+ | Var | Default | Meaning |
276
+ |-----|---------|---------|
277
+ | `BQ_MCP_ENVIRONMENTS` | _(none)_ | JSON map of environment name to settings. Takes precedence over the config file. |
278
+ | `BQ_MCP_DEFAULT_ENVIRONMENT` | _(safest, else first)_ | Environment used when a call omits `environment`. Prefers a staging/dev environment when unset. |
279
+ | `BQ_MCP_CONFIG` | `~/.config/data-platform-mcp/config.toml` | Path to the TOML config file |
280
+ | `BQ_IMPERSONATE_SERVICE_ACCOUNT` | _(none)_ | Read-only service account to impersonate |
281
+ | `BQ_PROJECT` | _(ADC project)_ | GCP project ID whose BigQuery datasets you query. Falls back to the project associated with your credentials; tools error with instructions if neither is set. |
282
+ | `BQ_LOCATION` | `US` | BigQuery location |
283
+ | `BQ_WARN_BYTES` | `1073741824` (1 GB) | Above this, ask the user to confirm before running |
284
+ | `BQ_MAX_BYTES_BILLED` | `5368709120` (5 GB) | Hard per-query scan cap — never exceeded |
285
+ | `BQ_COST_PER_TIB_USD` | `6.25` | On-demand price used to render the cost estimate |
286
+ | `BQ_ROW_LIMIT` | `200` | Default rows returned |
287
+ | `BQ_DATASET_ALLOWLIST` | _(empty = all)_ | Comma-separated dataset IDs |
288
+ | `BQ_MCP_TRANSPORT` | `stdio` | `stdio` (subprocess) or `http`/`sse` (serve over network) |
289
+ | `BQ_MCP_HOST` | `127.0.0.1` | Bind host when transport is `http`/`sse`. `run-http.sh` overrides this to `0.0.0.0` so containers can reach it — see the security note below. |
290
+ | `BQ_MCP_PORT` | `8765` | Bind port when transport is `http`/`sse` |
291
+ | `BQ_MCP_AUDIT_LOG` | `~/.local/state/data-platform-mcp/audit.jsonl` | JSONL record of every tool call. `off` disables it. SQL text is never written — only a hash and length. |
292
+ | `BQ_MCP_LOG_LEVEL` | `INFO` | Verbosity of the stderr log |
293
+
294
+ By default the server speaks **stdio** — the right choice when a client spawns
295
+ it (Claude Code, Claude Desktop), and what the Quick start above uses.
296
+
297
+ ---
298
+
299
+ ## Advanced: serve over HTTP
300
+
301
+ To reach the server from a **remote or containerized** client instead of having
302
+ each client spawn its own, run it over HTTP:
303
+
304
+ ```bash
305
+ BQ_PROJECT=your-gcp-project ./run-http.sh
306
+ # Serving … on http://0.0.0.0:8765/mcp
307
+ ```
308
+
309
+ Clients then connect by URL (Claude Code):
310
+
311
+ ```bash
312
+ claude mcp add --transport http bigquery http://<host>:8765/mcp
313
+ ```
314
+
315
+ > ⚠️ **Security:** the HTTP endpoint has **no authentication**, and every query
316
+ > runs under the **host's** ADC credentials — not the connecting user's. Anyone
317
+ > who can reach the port gets full read access to `BQ_PROJECT` under your
318
+ > identity. Only expose it on a trusted network (bind `BQ_MCP_HOST=127.0.0.1`
319
+ > and use an SSH tunnel/VPN, or an authenticating proxy). See
320
+ > [docs/nanoclaw.md](docs/nanoclaw.md) for the containerized-client setup this
321
+ > mode was designed for.
322
+
323
+ For server deployments, point `GOOGLE_APPLICATION_CREDENTIALS` at a
324
+ service-account key with BigQuery Data Viewer + Job User roles instead of using
325
+ personal ADC.
326
+
327
+ ---
328
+
329
+ ## Development
330
+
331
+ ```bash
332
+ ./.venv/bin/pip install -e ".[dev]"
333
+ ./.venv/bin/python -m pytest
334
+ ```
335
+
336
+ The suite needs **no credentials and no network** — every test runs against
337
+ fakes in `tests/conftest.py`, so it is deterministic and free. Layers:
338
+
339
+ | File | Covers |
340
+ |------|--------|
341
+ | `test_protocol.py` | The MCP contract through a real in-memory client session: tool set, read-only annotations, generated schemas, `isError` on refusal |
342
+ | `test_query_guard.py` | The cost gate — what runs, what is refused, what is handed back to the user, and what the caller is told about limits |
343
+ | `test_payload_shape.py` | Response shapes against fake tables, including the partitioning trap and nested-field flattening |
344
+ | `test_observability.py` | The audit trail, and the promise that SQL text never reaches it |
345
+ | `test_diagnostics.py` | `doctor`'s report, including the region and allowlist failures it exists to catch early |
346
+ | `test_environments.py` | Routing between environments, per-environment limits, and impersonation targeting |
347
+ | `test_config.py` | The environment registry, aliases, the TOML file, and the missing-project error that used to be an import-time crash |
348
+ | `test_errors.py` | Auth failures carry the command that fixes them |
349
+ | `test_formatting.py` | The size and cost figures a user is asked to approve |
350
+ | `test_eval_scoring.py` | The eval scorer, fed the trajectories each case exists to reject |
351
+
352
+ ### Evals
353
+
354
+ Two further layers need live credentials, so they are not part of `pytest`:
355
+ `evals/measure.py` records what a client actually receives from each tool, and
356
+ `evals/tool_use_evals.py` asks real questions through the `claude` CLI and
357
+ scores the **trajectory** from the server's own audit log — which tool ran,
358
+ against which environment, with which arguments.
359
+
360
+ ```bash
361
+ ./.venv/bin/python evals/measure.py # payload sizes
362
+ ./.venv/bin/python evals/tool_use_evals.py # 6 cases, spends tokens
363
+ ./.venv/bin/python evals/tool_use_evals.py --rescore # re-score saved replies, free
364
+ ```
365
+
366
+ See [`evals/README.md`](evals/README.md) for what each case catches and
367
+ [`evals/BASELINE.md`](evals/BASELINE.md) for what the last run measured. Tool
368
+ and server descriptions are the highest-leverage thing to change in this
369
+ server, and nothing except an eval tells you they need changing.
370
+
371
+ ### Mutation testing
372
+
373
+ A suite that passes on its first run proves nothing, so the guarantees above
374
+ were checked by breaking them: reverting refusals to error-shaped returns,
375
+ logging raw SQL, guessing partitioning from column names, removing the response
376
+ budget, dropping `functools.wraps` from the audit wrapper, letting confirmation
377
+ bypass the hard cap, silencing stale-table detection, and removing the
378
+ allowlist check. Each one fails the suite.
379
+
380
+ ## Releasing
381
+
382
+ Version numbers live in two files and CI refuses a tag where they disagree — a
383
+ mismatch would ship a tag pointing at different code than the package claims.
384
+ (`__version__` is read from the installed distribution, so it cannot drift.)
385
+
386
+ ```bash
387
+ # 1. bump both to the same value
388
+ # pyproject.toml project.version
389
+ # server.json version AND packages[0].version
390
+
391
+ # 2. tag and push
392
+ git tag v0.2.0 && git push origin v0.2.0
393
+ ```
394
+
395
+ The tag triggers `.github/workflows/release.yml`, which verifies the versions
396
+ agree, builds, publishes to PyPI via **Trusted Publishing**, then registers the
397
+ release with the **MCP registry**. Neither step stores a token: PyPI uses OIDC
398
+ from this repository and the `pypi` environment, and the registry uses GitHub
399
+ OIDC. Both need one-time setup before the first release:
400
+
401
+ - **PyPI:** add a trusted publisher at
402
+ <https://pypi.org/manage/account/publishing/> for repository
403
+ `deBilla/bigquery-mcp`, workflow `release.yml`, environment `pypi`.
404
+ - **GitHub:** create the `pypi` environment in repository settings.
405
+
406
+ ### What CI checks
407
+
408
+ `.github/workflows/ci.yml` runs on every push and pull request:
409
+
410
+ | Job | Checks |
411
+ |-----|--------|
412
+ | `test` | The suite on Python 3.11, 3.12 and 3.13 — with no GCP credentials on the runner, which is the point |
413
+ | `safety` | No credential-shaped strings in tracked files; `.env`/`.mcp.json` untracked; **no mutating BigQuery client calls anywhere in `src/`** |
414
+ | `package` | Builds, `twine check`s, asserts no local config leaked into the sdist, then installs the wheel into a clean venv and drives the real protocol — 5 tools, every one annotated read-only and documented, instructions intact |
415
+
416
+ The last one is the important one: it catches a package that installs cleanly
417
+ and dies on its first request, which is a failure no unit test sees.
418
+
419
+ ## License
420
+
421
+ MIT — see [LICENSE](LICENSE).