{
  "markdown": "# BigQuery MCP\n\n[![CI](https://github.com/deBilla/bigquery-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/deBilla/bigquery-mcp/actions/workflows/ci.yml)\n\nA **read-only** [Model Context Protocol](https://modelcontextprotocol.io) server over\nGoogle BigQuery. It lets an AI client (Claude Code, Claude Desktop, …) answer\nplain-language data questions by discovering schema and running `SELECT` queries.\n\nThe AI does the natural-language → SQL translation; this server just safely\nexecutes against BigQuery under **your own** Google credentials.\n\n- **Docs:** https://debilla.github.io/bigquery-mcp/\n- **Repo:** https://github.com/deBilla/bigquery-mcp\n- **PyPI:** [`data-platform-mcp`](https://pypi.org/project/data-platform-mcp/)\n- **MCP registry:** mcp-name: io.github.deBilla/data-platform-mcp\n\n---\n\n## Tools exposed\n\n| Tool | Purpose | Cost |\n|------|---------|------|\n| `list_datasets` | List datasets in the project | free |\n| `list_tables` | List tables/views in a dataset | free |\n| `get_table_schema` | Columns (nested paths expanded), **partitioning**, size, row count | free |\n| `check_table_freshness` | When each table was last written — catches stale sources | free |\n| `list_environments` | Which BigQuery environments are configured, and the default | free |\n| `list_scheduled_queries` | Which scheduled query writes a table, and whether it is disabled or failing | free |\n| `get_scheduled_query` | One query's SQL, destination and recent runs | free |\n| `run_query` | Run a validated, read-only `SELECT` and return rows | scans data |\n\nOnly `run_query` costs anything, so the discovery tools are the ones to spend\nfirst. Two of them exist to prevent specific, repeated mistakes:\n\n- **`get_table_schema` reports partitioning from table metadata, never from\n  column names.** A table with a `partition_date` column may not be partitioned\n  — in which case no `WHERE` clause reduces the scan and every query reads the\n  whole table. The response flags this explicitly when the table is large.\n- **`check_table_freshness` finds tables that stopped being written to** without\n  being dropped. Those return stale data rather than an error, which is the\n  failure mode nobody notices.\n- **`list_scheduled_queries` says why.** A stale table is usually a scheduled\n  query that was disabled or is failing, and that lives in a different API\n  (BigQuery Data Transfer) needing `roles/bigquerydatatransfer.viewer`. Without\n  that role the two tools return an error naming it and everything else works\n  normally. Most scheduled queries declare no destination because they write\n  with DDL, so the target is read out of the SQL and reported as\n  `writes_to_from_sql` — a heuristic, labelled as one.\n\n---\n\n## Environments\n\nOne server answers questions about several targets — a warehouse and its\nstaging copy, or two regions of the same project. Every tool takes an optional\n`environment`; omitting it uses the default.\n\n```toml\n# ~/.config/data-platform-mcp/config.toml\ndefault_environment = \"warehouse\"\n\n[environments.warehouse]\nproject = \"my-data-platform\"\nimpersonate = \"data-platform-mcp-ro@my-data-platform.iam.gserviceaccount.com\"\ndataset_allowlist = [\"sales\", \"events\"]\n\n[environments.central]          # same project, different region\nproject = \"my-data-platform\"\nlocation = \"us-central1\"\n```\n\nSee [`config.toml.example`](config.toml.example) for every setting, or set\n`BQ_MCP_ENVIRONMENTS` to the same structure as JSON. **A single `BQ_PROJECT`\nstill works unchanged** — it becomes one environment named `default`.\n\nAn environment can be named by its own name, an alias, the built-in shorthands\n(`prod`, `stg`, `dev`, `live`) or its project id. An **unknown** name is an\nerror naming the valid options, never a silent fall back to the default: a typo\nthat answered a production question from staging would be invisible in the\nreply. Every result echoes back the environment it came from.\n\nRegions are why this matters most here. BigQuery cannot query across locations,\nand its error for trying names neither location, so it reads as a missing\ntable. One environment per location; `doctor` reports which datasets are where.\n\n---\n\n## Read-only as a property of the identity\n\nThe SELECT-only guard and the `readOnlyHint` annotations are promises about\nthis code. Pointing the server at a service account that holds only\n`roles/bigquery.jobUser` and a dataset-scoped `roles/bigquery.dataViewer` makes\nit a fact about the credentials — enforced by IAM whatever the code does, and\nwhatever your own roles allow:\n\n```bash\ndata-platform-mcp setup --project my-data-platform --datasets sales,events\n```\n\nCreates the account, grants those two roles, and gives you\n`roles/iam.serviceAccountTokenCreator` on it so the server can impersonate it.\nAdd `--dry-run` to see the commands first; it is safe to re-run.\n\nWith `--datasets`, the dataset allowlist stops being an `if` statement in this\nprocess and becomes a grant Google enforces.\n\n---\n\n## macOS setup\n\n**Terminal.app is not Xcode.** It ships with every Mac. What does *not* ship is\nthe **Xcode Command Line Tools**, and an analyst's laptop usually has neither\nthose nor Homebrew. Nothing here needs them — but it is easy to trip over by\naccident, because `git`, `make`, `clang` and the stock `/usr/bin/python3` are\nstubs for that bundle: running any of them pops a system dialog offering to\ninstall about a gigabyte of developer tooling.\n\nNone of the commands below invoke one. They use only utilities macOS already\nhas — `curl`, `tar`, `sh`, `uname` — because both installs are self-contained:\n\n| Install | Why it needs nothing else |\n| --- | --- |\n| `uv` | A standalone binary. Its installer never mentions Python, and it downloads its own to run the server. |\n| Google Cloud CLI | The macOS tarball bundles its own Python (`.install/bundled-python3-unix-darwin-*`). |\n\nThe whole terminal requirement is the three blocks below, once.\n\n### 1. Install uv\n\n```bash\ncurl -LsSf https://astral.sh/uv/install.sh | sh\nwhich uvx      # note this absolute path — Claude Desktop will need it\n```\n\nTypically `/Users/<you>/.local/bin/uvx`.\n\n### 2. Install the Google Cloud CLI\n\nPick the build for your chip — `uname -m` prints `arm64` for Apple Silicon,\n`x86_64` for Intel:\n\n```bash\n# Apple Silicon\ncurl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-darwin-arm.tar.gz\ntar -xzf google-cloud-cli-darwin-arm.tar.gz\n\n# Intel — same, with the other file\n# curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-darwin-x86_64.tar.gz\n# tar -xzf google-cloud-cli-darwin-x86_64.tar.gz\n\n./google-cloud-sdk/install.sh --quiet\n```\n\nAvoid `brew install --cask google-cloud-sdk`: Homebrew itself requires the\nCommand Line Tools, which is the thing this section exists to avoid.\n\n### 3. Authenticate\n\n```bash\n./google-cloud-sdk/bin/gcloud auth application-default login\n./google-cloud-sdk/bin/gcloud auth application-default set-quota-project your-gcp-project\n```\n\nThis writes a credentials file that the Google libraries read directly.\n**`gcloud` does not need to be on your `PATH` afterwards** — it is needed once,\nhere. That is why a GUI-launched Claude Desktop can query BigQuery even though\nit cannot see your shell.\n\n> Your account needs **BigQuery Job User** on the project the query runs in, and\n> **BigQuery Data Viewer** on each dataset it reads — often a different project.\n\n### 4. Check it worked\n\n```bash\nBQ_PROJECT=your-gcp-project uvx data-platform-mcp doctor\n```\n\nThen register with your client: [Claude Desktop](#claude-desktop) or\n[Claude Code](#4-register-with-your-ai-client).\n\n### Alternative: no terminal at all for the analyst\n\nIf even that is too much, an admin can do the credential half centrally and\nthe analyst installs nothing but `uv` — skipping step 2 and step 3 entirely.\n(Nothing about this is macOS-specific; it works the same on any OS.)\n\n```bash\n# the admin, once, on their own machine\ndata-platform-mcp setup --project your-gcp-project --datasets sales,events\ngcloud iam service-accounts keys create analyst-key.json \\\n  --iam-account data-platform-mcp-ro@your-gcp-project.iam.gserviceaccount.com\n```\n\nThe analyst saves that file and points the config at it:\n\n```json\n{\n  \"mcpServers\": {\n    \"bigquery\": {\n      \"command\": \"/Users/YOU/.local/bin/uvx\",\n      \"args\": [\"data-platform-mcp\"],\n      \"env\": {\n        \"BQ_PROJECT\": \"your-gcp-project\",\n        \"GOOGLE_APPLICATION_CREDENTIALS\": \"/Users/YOU/keys/analyst-key.json\"\n      }\n    }\n  }\n}\n```\n\n**The trade-off is real and worth stating.** A key file is a long-lived\ncredential sitting on a laptop, where `gcloud auth application-default login`\nissues short-lived tokens tied to a person. It is defensible here because the\naccount created by `setup --datasets` can only read the datasets you name, and\nbecause a key can be revoked centrally the moment a laptop is lost — but it is\nstrictly weaker, and it is a per-analyst secret, so do not put it in a shared\nconfig file or a repository.\n\n\n---\n\n## Linux\n\nThe same two installs, with no equivalent of the Command Line Tools problem:\n\n```bash\ncurl -LsSf https://astral.sh/uv/install.sh | sh\ncurl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz\ntar -xzf google-cloud-cli-linux-x86_64.tar.gz\n./google-cloud-sdk/install.sh --quiet\n./google-cloud-sdk/bin/gcloud auth application-default login\n```\n\nThen [check it worked](#4-check-it-worked) and register with your client.\n\n---\n\n## Windows\n\n> **Not verified end to end.** The download URLs and install locations below\n> were checked; the flow itself has not been run on a Windows machine. CI tests\n> Linux only. Treat this as a careful derivation, not a tested recipe — and\n> please open an issue if a step is wrong.\n\n### 1. Install uv\n\nIn PowerShell:\n\n```powershell\npowershell -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n```\n\nThis installs `uv.exe` and `uvx.exe` into `%USERPROFILE%\\.local\\bin`. Confirm\nthe exact path, because the Desktop config needs it in full:\n\n```powershell\n(Get-Command uvx).Source\n```\n\n### 2. Install the Google Cloud CLI\n\nDownload and run\n[GoogleCloudSDKInstaller.exe](https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe).\nLeave **\"Bundled Python\"** ticked — it is what lets the SDK run without a\nseparate Python install, the same property the macOS tarball has.\n\n### 3. Authenticate\n\nIn a **new** PowerShell window, so it picks up the updated `PATH`:\n\n```powershell\ngcloud auth application-default login\ngcloud auth application-default set-quota-project your-gcp-project\n```\n\nThis writes credentials to\n`%APPDATA%\\gcloud\\application_default_credentials.json`, which the Google\nlibraries read directly — so `gcloud` need not be on `PATH` afterwards.\n\n### 4. Check it worked\n\n```powershell\n$env:BQ_PROJECT=\"your-gcp-project\"; uvx data-platform-mcp doctor\n```\n\n### 5. Configure Claude Desktop\n\n`%APPDATA%\\Claude\\claude_desktop_config.json` — create it if absent.\n**Backslashes must be doubled in JSON**, and the path must be absolute:\n\n```json\n{\n  \"mcpServers\": {\n    \"bigquery\": {\n      \"command\": \"C:\\\\Users\\\\YOU\\\\.local\\\\bin\\\\uvx.exe\",\n      \"args\": [\"data-platform-mcp\"],\n      \"env\": {\n        \"BQ_PROJECT\": \"your-gcp-project\"\n      }\n    }\n  }\n}\n```\n\nReplace `C:\\Users\\YOU\\...` with what `(Get-Command uvx).Source` printed, with\neach `\\` written as `\\\\`. Then fully quit and reopen Claude Desktop.\n\nIf it fails, the logs are in `%APPDATA%\\Claude\\logs\\`. `ENOENT` there means the\n`command` path is wrong or its backslashes were not doubled — the same failure\nmacOS has, with one extra way to get it wrong.\n\n---\n\n## Quick start (per user)\n\nEach person runs their own local copy. Queries execute under **their own**\nBigQuery/IAM permissions, so existing access controls decide who can see what.\n\nInstall the prerequisites for your platform first — [macOS](#macos-setup),\n[Linux](#linux), [Windows](#windows) — then come back here.\n\n### 1. Install\n\nThe package is published as **`data-platform-mcp`** (`bigquery-mcp` was already\ntaken on PyPI by an unrelated project). No checkout is needed — the client can\nfetch and run it directly:\n\n```bash\nuvx data-platform-mcp --version\n```\n\n**From source**, for development:\n\n```bash\ngit clone git@github.com:deBilla/bigquery-mcp.git\ncd bigquery-mcp\n\npython3 -m venv .venv\n./.venv/bin/pip install -e .\n```\n\nEither way you get a `data-platform-mcp` command, which is what the client runs.\n\n### 2. Authenticate to Google (one time)\n\nCovered in the platform sections above: [macOS step 3](#3-authenticate), or the\nequivalent `gcloud auth application-default login` elsewhere. Queries then run\nunder your own credentials via\n[Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials).\n\n**Using a service-account key instead?** Set `GOOGLE_APPLICATION_CREDENTIALS`\nto its path — but set it **where the MCP server is launched**, not in a shell:\n\n```jsonc\n// in your client's MCP config, alongside BQ_PROJECT\n\"env\": {\n  \"BQ_PROJECT\": \"your-gcp-project\",\n  \"GOOGLE_APPLICATION_CREDENTIALS\": \"/absolute/path/to/key.json\"\n}\n```\n\nThe client spawns the server as a subprocess with only the environment its\nconfig declares. Exporting the variable in a terminal has no effect on it —\nthat is a distinct failure from having no credentials at all, and it looks\nidentical from the outside.\n\n### 3. Check your setup\n\n```bash\nBQ_PROJECT=your-gcp-project data-platform-mcp doctor\n```\n\nChecks credentials, job permission, dataset visibility and — the one that\ncatches people — **dataset regions**. BigQuery cannot query a dataset from a\ndifferent location, and its own error names neither the location it wanted nor\nthe one the dataset is in, so it reads as a missing table. `doctor` names both:\n\n```\n[  ok  ] run a query in my-project (location US)\n[  ok  ] 39 datasets visible (no allowlist; all are readable)\n[ warn ] 6 of 39 datasets are outside location US\n         US-CENTRAL1: analytics_raw, business_data, ds_public, pg_public, public, recommendations\n         BigQuery cannot query these from US, and cannot join them with\n         datasets that are in it.\n         Fix:  set BQ_LOCATION to the region you need, and run a separate\n               server for datasets in another one.\n```\n\nA dataset in another region is a warning; one on your `BQ_DATASET_ALLOWLIST` is\na failure, because no tool call could ever read it.\n\n### 4. Register with your AI client\n\nReplace `your-gcp-project` with your GCP project ID.\n\n**Claude Code** — once published:\n\n```bash\nclaude mcp add bigquery \\\n  --env BQ_PROJECT=your-gcp-project \\\n  -- uvx data-platform-mcp\n```\n\nFrom a source install, point at the checkout instead (replace\n`/abs/path/bigquery-mcp`):\n\n```bash\nclaude mcp add bigquery \\\n  --env BQ_PROJECT=your-gcp-project \\\n  -- /abs/path/bigquery-mcp/.venv/bin/data-platform-mcp\n```\n\n**Claude Desktop** — see the dedicated section below; it needs absolute paths.\n\n### 5. Restart the client and ask a question\n\n> \"Which datasets are available? In the `sales` dataset, how many rows does the\n> `orders` table have?\"\n\n---\n\n## Claude Desktop\n\nMost of a data team will use Desktop rather than the CLI, and it has one\nfailure mode the CLI does not.\n\n**Claude Desktop does not inherit your shell `PATH`.** It launches from the\nFinder, so `uvx`, `python` and anything installed by Homebrew or `uv` are\ninvisible to it. A config that says `\"command\": \"uvx\"` fails with `ENOENT` —\nthe server never starts, and the error names the command rather than the\nreason. **Every path in this file must be absolute.**\n\nWhat does *not* break: **credentials**. Application Default Credentials are a\nfile that the Google libraries read directly, so `gcloud` does not need to be\non `PATH` for queries to work — it is only needed once, in a terminal, to\ncreate that file. Verified by running this server with an entirely empty\nenvironment: the query succeeded.\n\n### 1. Install and authenticate\n\nDo the platform setup first — [macOS](#macos-setup) (two pastes, no Xcode\ntools needed) or [Linux](#linux), [Windows](#windows). You need two things\nfrom it: the **absolute path** that `which uvx` printed, and a completed\n`gcloud auth application-default login`.\n\n### 2. Edit the config\n\nClaude Desktop's **Settings → Connectors** lists hosted connectors; a local\nserver like this one is not added there. It goes in a JSON file instead:\n\n**Settings → Developer → Edit Config** opens it. Or edit it directly:\n\n| OS | File |\n| --- | --- |\n| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |\n| Windows | `%APPDATA%\\Claude\\claude_desktop_config.json` |\n\n**The file usually already exists and holds your Desktop preferences.** Add\n`mcpServers` as one more top-level key — do not replace the file, or you will\nlose those settings. If it genuinely does not exist, create it with just the\nblock below.\n\n```json\n{\n  \"mcpServers\": {\n    \"bigquery\": {\n      \"command\": \"/Users/YOU/.local/bin/uvx\",\n      \"args\": [\"data-platform-mcp\"],\n      \"env\": {\n        \"BQ_PROJECT\": \"your-gcp-project\"\n      }\n    }\n  }\n}\n```\n\nReplace `/Users/YOU/.local/bin/uvx` with what `which uvx` printed. On Windows\nthe path looks like `C:\\\\Users\\\\YOU\\\\.local\\\\bin\\\\uvx.exe`, and backslashes must be\ndoubled in JSON.\n\nMerged into a file that already has settings, it looks like this — `mcpServers`\nsits alongside whatever is there, not instead of it:\n\n```json\n{\n  \"preferences\": { \"...\": \"your existing settings, left alone\" },\n  \"mcpServers\": {\n    \"bigquery\": {\n      \"command\": \"/Users/YOU/.local/bin/uvx\",\n      \"args\": [\"data-platform-mcp\"],\n      \"env\": { \"BQ_PROJECT\": \"your-gcp-project\" }\n    }\n  }\n}\n```\n\nCheck it still parses before restarting — a stray comma disables **every**\nserver, silently:\n\n```bash\npython3 -m json.tool ~/Library/Application\\ Support/Claude/claude_desktop_config.json\n```\n\n### 3. Restart Claude Desktop\n\nFully quit and reopen — reloading the window is not enough. The server appears\nunder the tools icon in the message box.\n\n### Managing several warehouses\n\nRather than growing the JSON, put the environments in\n`~/.config/data-platform-mcp/config.toml` (see\n[Environments](#environments)). The Desktop config then needs no `env` block at\nall, and is identical on every machine:\n\n```json\n{\n  \"mcpServers\": {\n    \"bigquery\": {\n      \"command\": \"/Users/YOU/.local/bin/uvx\",\n      \"args\": [\"data-platform-mcp\"]\n    }\n  }\n}\n```\n\nThis is the better shape for a team: one config file to share, and the JSON\nstops carrying project ids.\n\n### When it does not work\n\nDesktop hides the reason, so check in this order:\n\n1. **Run the doctor in a terminal.** It reports credentials, roles, dataset\n   visibility and regions in one pass, and is the fastest way to tell a setup\n   problem from a Desktop problem:\n   ```bash\n   BQ_PROJECT=your-gcp-project /Users/YOU/.local/bin/uvx data-platform-mcp doctor\n   ```\n2. **Read the logs.** macOS: `~/Library/Logs/Claude/mcp*.log`. `ENOENT` or\n   \"command not found\" there means the `command` path is wrong — go back to\n   `which uvx`.\n3. **Check the JSON parses.** A trailing comma silently disables every server:\n   ```bash\n   python3 -m json.tool ~/Library/Application\\ Support/Claude/claude_desktop_config.json\n   ```\n\n\n## Safety\n\n- Every query is **dry-run first** to validate it and estimate bytes scanned.\n- **Only `SELECT` / `WITH`** statements run — no writes, DDL, or DML.\n- **Cost confirmation:** a query estimated to scan more than `BQ_WARN_BYTES`\n  (default 1 GB) does **not** run. It returns `status: \"confirmation_required\"`\n  with the estimated scan size and dollar cost so the client can ask before\n  proceeding. Re-call with `confirm_expensive=true` to run it.\n- **Hard cap:** queries above `BQ_MAX_BYTES_BILLED` (default 5 GB) never run,\n  even with confirmation — a runaway-cost backstop.\n- Optional **dataset allowlist** restricts what can be read.\n- **Refusals are protocol errors.** Anything the server declines to do — a\n  non-`SELECT` statement, a disallowed dataset, a query over the hard cap —\n  arrives with MCP's `isError` set, so it cannot be mistaken for a result.\n  `confirmation_required` is the deliberate exception: it is a normal result,\n  because the agent is meant to relay it and come back.\n- **Responses are size-bounded.** `run_query` stops adding rows once the\n  serialised response reaches ~40k characters and sets `stopped_for_size`, so a\n  wide result cannot quietly consume the whole context window. A partial answer\n  always says that it is partial.\n- **SQL is never written to the audit log** — only a hash and a length. Query\n  text routinely contains the user IDs or emails it filters on.\n\n### Cost-confirmation flow\n\n```\nrun_query(sql)\n   │  dry run estimates the scan\n   ├── ≤ 1 GB ........... runs, returns rows + estimated_cost_usd\n   ├── 1–5 GB .......... status: confirmation_required (size + $ estimate) → ask user\n   │                      → run_query(sql, confirm_expensive=true) runs it\n   └── > 5 GB ........... rejected, never runs\n```\n\n---\n\n## Configuration (environment variables)\n\n| Var | Default | Meaning |\n|-----|---------|---------|\n| `BQ_MCP_ENVIRONMENTS` | _(none)_ | JSON map of environment name to settings. Takes precedence over the config file. |\n| `BQ_MCP_DEFAULT_ENVIRONMENT` | _(safest, else first)_ | Environment used when a call omits `environment`. Prefers a staging/dev environment when unset. |\n| `BQ_MCP_CONFIG` | `~/.config/data-platform-mcp/config.toml` | Path to the TOML config file |\n| `BQ_IMPERSONATE_SERVICE_ACCOUNT` | _(none)_ | Read-only service account to impersonate |\n| `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. |\n| `BQ_LOCATION` | `US` | BigQuery location |\n| `BQ_WARN_BYTES` | `1073741824` (1 GB) | Above this, ask the user to confirm before running |\n| `BQ_MAX_BYTES_BILLED` | `5368709120` (5 GB) | Hard per-query scan cap — never exceeded |\n| `BQ_COST_PER_TIB_USD` | `6.25` | On-demand price used to render the cost estimate |\n| `BQ_ROW_LIMIT` | `200` | Default rows returned |\n| `BQ_DATASET_ALLOWLIST` | _(empty = all)_ | Comma-separated dataset IDs |\n| `BQ_MCP_TRANSPORT` | `stdio` | `stdio` (subprocess) or `http`/`sse` (serve over network) |\n| `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. |\n| `BQ_MCP_PORT` | `8765` | Bind port when transport is `http`/`sse` |\n| `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. |\n| `BQ_MCP_LOG_LEVEL` | `INFO` | Verbosity of the stderr log |\n\nBy default the server speaks **stdio** — the right choice when a client spawns\nit (Claude Code, Claude Desktop), and what the Quick start above uses.\n\n---\n\n## Advanced: serve over HTTP\n\nTo reach the server from a **remote or containerized** client instead of having\neach client spawn its own, run it over HTTP:\n\n```bash\nBQ_PROJECT=your-gcp-project ./run-http.sh\n# Serving … on http://0.0.0.0:8765/mcp\n```\n\nClients then connect by URL (Claude Code):\n\n```bash\nclaude mcp add --transport http bigquery http://<host>:8765/mcp\n```\n\n> ⚠️ **Security:** the HTTP endpoint has **no authentication**, and every query\n> runs under the **host's** ADC credentials — not the connecting user's. Anyone\n> who can reach the port gets full read access to `BQ_PROJECT` under your\n> identity. Only expose it on a trusted network (bind `BQ_MCP_HOST=127.0.0.1`\n> and use an SSH tunnel/VPN, or an authenticating proxy). See\n> [docs/nanoclaw.md](docs/nanoclaw.md) for the containerized-client setup this\n> mode was designed for.\n\nFor server deployments, point `GOOGLE_APPLICATION_CREDENTIALS` at a\nservice-account key with BigQuery Data Viewer + Job User roles instead of using\npersonal ADC.\n\n---\n\n## Development\n\n```bash\n./.venv/bin/pip install -e \".[dev]\"\n./.venv/bin/python -m pytest\n```\n\nThe suite needs **no credentials and no network** — every test runs against\nfakes in `tests/conftest.py`, so it is deterministic and free. Layers:\n\n| File | Covers |\n|------|--------|\n| `test_protocol.py` | The MCP contract through a real in-memory client session: tool set, read-only annotations, generated schemas, `isError` on refusal |\n| `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 |\n| `test_payload_shape.py` | Response shapes against fake tables, including the partitioning trap and nested-field flattening |\n| `test_observability.py` | The audit trail, and the promise that SQL text never reaches it |\n| `test_diagnostics.py` | `doctor`'s report, including the region and allowlist failures it exists to catch early |\n| `test_environments.py` | Routing between environments, per-environment limits, and impersonation targeting |\n| `test_config.py` | The environment registry, aliases, the TOML file, and the missing-project error that used to be an import-time crash |\n| `test_errors.py` | Auth failures carry the command that fixes them |\n| `test_formatting.py` | The size and cost figures a user is asked to approve |\n| `test_eval_scoring.py` | The eval scorer, fed the trajectories each case exists to reject |\n\n### Evals\n\nTwo further layers need live credentials, so they are not part of `pytest`:\n`evals/measure.py` records what a client actually receives from each tool, and\n`evals/tool_use_evals.py` asks real questions through the `claude` CLI and\nscores the **trajectory** from the server's own audit log — which tool ran,\nagainst which environment, with which arguments.\n\n```bash\n./.venv/bin/python evals/measure.py                     # payload sizes\n./.venv/bin/python evals/tool_use_evals.py              # 6 cases, spends tokens\n./.venv/bin/python evals/tool_use_evals.py --rescore    # re-score saved replies, free\n```\n\nSee [`evals/README.md`](evals/README.md) for what each case catches and\n[`evals/BASELINE.md`](evals/BASELINE.md) for what the last run measured. Tool\nand server descriptions are the highest-leverage thing to change in this\nserver, and nothing except an eval tells you they need changing.\n\n### Mutation testing\n\nA suite that passes on its first run proves nothing, so the guarantees above\nwere checked by breaking them: reverting refusals to error-shaped returns,\nlogging raw SQL, guessing partitioning from column names, removing the response\nbudget, dropping `functools.wraps` from the audit wrapper, letting confirmation\nbypass the hard cap, silencing stale-table detection, and removing the\nallowlist check. Each one fails the suite.\n\n## Releasing\n\nVersion numbers live in two files and CI refuses a tag where they disagree — a\nmismatch would ship a tag pointing at different code than the package claims.\n(`__version__` is read from the installed distribution, so it cannot drift.)\n\n```bash\n# 1. bump both to the same value\n#      pyproject.toml   project.version\n#      server.json      version  AND  packages[0].version\n\n# 2. tag and push\ngit tag v0.2.0 && git push origin v0.2.0\n```\n\nThe tag triggers `.github/workflows/release.yml`, which verifies the versions\nagree, builds, publishes to PyPI via **Trusted Publishing**, then registers the\nrelease with the **MCP registry**. Neither step stores a token: PyPI uses OIDC\nfrom this repository and the `pypi` environment, and the registry uses GitHub\nOIDC. Both need one-time setup before the first release:\n\n- **PyPI:** add a trusted publisher at\n  <https://pypi.org/manage/account/publishing/> for repository\n  `deBilla/bigquery-mcp`, workflow `release.yml`, environment `pypi`.\n- **GitHub:** create the `pypi` environment in repository settings.\n\n### What CI checks\n\n`.github/workflows/ci.yml` runs on every push and pull request:\n\n| Job | Checks |\n|-----|--------|\n| `test` | The suite on Python 3.11, 3.12 and 3.13 — with no GCP credentials on the runner, which is the point |\n| `safety` | No credential-shaped strings in tracked files; `.env`/`.mcp.json` untracked; **no mutating BigQuery client calls anywhere in `src/`** |\n| `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 |\n\nThe last one is the important one: it catches a package that installs cleanly\nand dies on its first request, which is a failure no unit test sees.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n",
  "bytes": 28611,
  "sha": "96f6967fd8dce5511bdd6e92f3f6996d18e311f5ed5c369c29a87224fc5c33b6",
  "repo_slug": "debilla/bigquery-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_debilla_data_platform_mcp_93ee37e1/readme"
}