bq-cost-guard
by yashas004
README.md
# bq-cost-guard
An [MCP](https://modelcontextprotocol.io) server that estimates the cost of a
BigQuery query **before you run it**. It exposes a single tool,
`estimate_query_cost`, which:
1. runs a BigQuery **dry run** (free — no data is read or billed) using your
Application Default Credentials,
2. reports **bytes processed** and the **estimated on-demand cost in USD**, and
3. returns **warnings** for common sources of wasted spend:
- `SELECT *` (and `SELECT t.*`) — scanning columns you don't need
- **No partition filter** — a partitioned table referenced by the query is
scanned without a filter on its partition column (or
`_PARTITIONTIME` / `_PARTITIONDATE`)
## Example output
```json
{
"bytesProcessed": 1099511627776,
"bytesProcessedHuman": "1.00 TiB",
"estimatedCostUsd": 6.25,
"currency": "USD",
"usdPerTib": 6.25,
"cacheHit": false,
"warnings": [
{
"type": "SELECT_STAR",
"message": "Query selects every column (SELECT *). Request only the columns you need to reduce bytes scanned."
},
{
"type": "NO_PARTITION_FILTER",
"table": "my-proj.sales.orders",
"partitionColumn": "created_at",
"message": "`my-proj.sales.orders` is partitioned by the partition column `created_at`, but the query has no filter on it, so the whole table will be scanned. Add a filter on the partition column (or _PARTITIONTIME / _PARTITIONDATE)."
}
]
}
```
Errors (invalid SQL, missing permissions, ...) are returned as normal tool
errors and never crash the server.
## Prerequisites
- **Node.js ≥ 22.18**
- A Google Cloud project with the **BigQuery API enabled**
- Permission to run dry runs (`bigquery.jobs.create`) and to read table
metadata (`bigquery.tables.get`) — the latter is only needed for the
partition-filter warnings; if metadata can't be read, the cost estimate
still works and the warning is silently skipped
## Install
```bash
git clone <this repo> bq-cost-guard
cd bq-cost-guard
npm install
npm run build
```
This produces the executable entry point at `dist/src/index.js`.
## Authentication (Application Default Credentials)
**No credentials live in this repository.** The server authenticates with
[Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials).
Pick one of:
1. **gcloud (recommended for development)**
```bash
gcloud auth application-default login
gcloud config set project your-project-id
```
2. **Service-account key file (kept outside the repo)**
```bash
export GOOGLE_APPLICATION_CREDENTIALS="$HOME/keys/bq-cost-guard-sa.json"
```
Point it at a key file stored anywhere *outside* the checkout — the
`.gitignore` in this repo blocks key files from being committed, but you
should still keep keys outside the project tree.
3. **Workload identity** — on GCE / GKE / Cloud Run / Cloud Shell, ADC is
provided automatically; set `GOOGLE_CLOUD_PROJECT` if it cannot be inferred.
## MCP client configuration
The server communicates over stdio. Replace `/absolute/path/to/bq-cost-guard`
with the real checkout path.
### Claude Desktop
Edit `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`,
Windows: `%APPDATA%\Claude\claude_desktop_config.json`):
```json
{
"mcpServers": {
"bq-cost-guard": {
"command": "node",
"args": ["/absolute/path/to/bq-cost-guard/dist/src/index.js"],
"env": {
"GOOGLE_CLOUD_PROJECT": "your-project-id"
}
}
}
}
```
### Claude Code
```bash
claude mcp add bq-cost-guard \
--env GOOGLE_CLOUD_PROJECT=your-project-id \
-- node /absolute/path/to/bq-cost-guard/dist/src/index.js
```
### Cursor
Add the same block to `~/.cursor/mcp.json` (project-level: `.cursor/mcp.json`):
```json
{
"mcpServers": {
"bq-cost-guard": {
"command": "node",
"args": ["/absolute/path/to/bq-cost-guard/dist/src/index.js"],
"env": {
"GOOGLE_CLOUD_PROJECT": "your-project-id"
}
}
}
}
```
Any other stdio MCP client can use the same `command`/`args`/`env` shape. If
you rely on a key file, add `"GOOGLE_APPLICATION_CREDENTIALS": "/abs/path/to/key.json"`
to `env` (or export it in the shell the client inherits).
## Environment variables
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `GOOGLE_CLOUD_PROJECT` | no | inferred | Default project for the BigQuery client |
| `GOOGLE_APPLICATION_CREDENTIALS` | no | — | Absolute path to a service-account key file (outside the repo) |
| `BQ_COST_GUARD_USD_PER_TIB` | no | `6.25` | On-demand price in USD per TiB scanned |
## Tool reference
### `estimate_query_cost`
| Argument | Type | Description |
| --- | --- | --- |
| `sql` | `string` (required) | BigQuery standard SQL to estimate |
Returns `bytesProcessed`, `bytesProcessedHuman`, `estimatedCostUsd`,
`currency`, `usdPerTib`, `cacheHit`, and `warnings[]` (see example above).
### How the warnings work
- **SELECT \*** — heuristic token scan of the SQL with comments and string
literals removed; `count(*)` and friends do not trigger it.
- **NO_PARTITION_FILTER** — the tool extracts `FROM`/`JOIN` table references
(skipping CTEs, subqueries, `UNNEST`, wildcard and INFORMATION_SCHEMA
references), reads each table's metadata to see whether it is partitioned
(time- or range-partitioned), and checks the query's `WHERE`/`ON` clauses
for the partition column or the `_PARTITIONTIME`/`_PARTITIONDATE` pseudo
columns. Tables whose metadata can't be read are skipped silently.
Both are best-effort heuristics meant to prompt a second look, not a full SQL
parser.
## Pricing notes
The cost figure is `bytesProcessed / 1024^4 × usdPerTib` at the **on-demand
list price** (default $6.25/TiB, override with `BQ_COST_GUARD_USD_PER_TIB`).
It ignores reservations/editions and flat-rate pricing, free-tier monthly
credits, storage and other charges, and result-cache effects (a `cacheHit`
flag is reported instead). Treat it as an estimate, not a bill.
## Development
```bash
npm test # unit tests (BigQuery client fully mocked, no network/GCP access)
npm run typecheck # tsc --noEmit
npm run build # emit dist/
npm start # run the built server on stdio
```
Tests use Node's built-in test runner with the BigQuery client replaced by a
mock of the exact client surface used here (`query({ dryRun: true })` and
`dataset().table().getMetadata()`), so the suite runs without credentials or
network access. The MCP protocol surface is tested end-to-end over an
in-memory transport.
Project layout:
```
src/
index.ts # entry point: wires real BigQuery client + stdio transport
server.ts # MCP server + estimate_query_cost tool registration
estimator.ts # bytes/cost/warnings orchestration
bigquery.ts # thin adapter over @google-cloud/bigquery (dry run + metadata)
sql.ts # pure SQL text analysis (SELECT *, table refs, partition filters)
cost.ts # pricing math and formatting
test/ # unit tests with the BigQuery client mocked
```
## License
[MIT](LICENSE)
TDQS
A4.2/5.0
Scored across 1 tool
Disambiguation5/5
There is only one tool, so there is no possibility of confusion or misselection. Its purpose (dry-run cost estimation) is unmistakable.
Naming Consistency5/5
The single name 'estimate_query_cost' follows a clean verb_noun convention and is descriptive. No inconsistent conventions exist to penalize.
Tool Count3/5
A single tool is thin even for a narrow 'cost guard' purpose; there is no way to estimate multiple queries, compare variants, or inspect table metadata. It functions but feels minimal.
Completeness3/5
Core dry-run cost estimation is covered with useful warnings, but the surface is a dead end: no schema/table inspection, partition discovery, or cost comparison to support the guard workflow. Notable gaps remain for a cost-management domain.
Maintenance
ActivityMaintained
ResponsivenessNo issues