Skip to main content
Glama
roymani06

GCP BigQuery MCP Server

by roymani06
README.md
# GCP BigQuery MCP Server (FastMCP + Microsoft Entra ID SSO)

A production-ready, enterprise-grade Model Context Protocol (MCP) server built in Python using **FastMCP**, **Streamable HTTP over `/mcp`**, and **Google Cloud BigQuery**.

---

## Architecture Overview

```
[ Developer / Claude Desktop / IDE / Inspector ]
│
│ 1. Client authenticates via Microsoft Entra ID (Azure AD SSO)
│    Sends Entra JWT: Authorization: Bearer <ACCESS_TOKEN>
▼
┌────────────────────────────────────────────────────────────────────────┐
│                   GCP BigQuery MCP Server (FastMCP)                    │
│ - Streamable HTTP mounted at configured endpoint (default: /mcp)       │
│ - Entra ID Auth Middleware:                                            │
│   • Configurable toggle: security.enable_auth (true/false)             │
│   • Fetches & caches Entra JWKS keys (login.microsoftonline.com)       │
│   • Validates RS256 signature, audience, accepted issuers & expiration │
│   • Extracts user identity (upn / email / oid)                         │
│ - Dynamically exposes ONLY the tools toggled ON in config.yaml         │
│ - Enforces Dual SQL Sanitizer (Regex keywords + AST syntax tree)       │
│ - Connects to BigQuery directly via Service Account JSON key credentials│
│ - Binds queries to max_bytes_billed and safe max_results pagination   │
│ - In-memory TTLCache with SHA-256 hashed keys for metadata queries     │
└───────────────────────────────────┬────────────────────────────────────┘
│
│ 2. Direct Machine-to-Machine 2-Legged Auth
│    (bigquery.Client.from_service_account_json)
▼
┌────────────────────────────────────────────────────────────────────────┐
│                       Google Cloud BigQuery                            │
│ - Enforces permissions via Service Account IAM & Dataset ACLs          │
│ - Executes read queries strictly within max_bytes_billed limit         │
└────────────────────────────────────────────────────────────────────────┘
```

---

## Key Features

1. **Inbound SSO with Microsoft Entra ID (Azure AD)**:
   - Cryptographic verification against Microsoft's public JWKS endpoint.
   - Validates RS256 signature, audience (`<client_id>` or `api://<client_id>`), v1.0 and v2.0 token issuers, and expiration.
   - **Configurable Auth Toggle (`security.enable_auth`)**: Easily toggle off auth (`false`) for local debugging and MCP Inspector testing, or enforce it in production (`true`).
2. **Direct BigQuery Service Account Integration**:
   - Zero interactive OAuth overhead; 2-legged server-to-server authentication directly initialized from the JSON service account key.
   - Dataset and column-level security enforced natively on Google Cloud.
3. **Execution Guardrails & Cost Ceiling**:
   - `max_bytes_billed` hard cost limit attached to `QueryJobConfig` (default: 10 GB).
   - `dry_run=True` simulation mode calculating query cost and cache hits with zero billing.
   - Container OOM protection by passing `max_results` into `query_job.result()`.
4. **Dual SQL Sanitizer**:
   - Enforces read-only query execution through both fast word-boundary regex filtering and deep BigQuery AST parsing (`sqlglot`).
   - Blocks `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `MERGE`, `GRANT`, `REVOKE`, `EXECUTE`, `CALL`, multi-query chaining, and mutations nested inside CTEs/subqueries.
5. **Dynamic Tool Enablement & Sanitized Error Shielding**:
   - Each BigQuery tool (`bq_list_datasets`, `bq_list_tables`, `bq_table_metadata`, `bq_query_execution`) can be selectively enabled or disabled via `config/config.yaml`.
   - All tool invocations are shielded with `try/except` mapping Google API and internal errors to sanitized `fastmcp.exceptions.ToolError` instances, preventing internal GCP endpoints, tracebacks, credentials paths, and queries from leaking to clients.
6. **Metadata Caching & Concurrency Hardening**:
   - In-memory `TTLCache` with deterministic SHA-256 key hashing to minimize BigQuery API metadata calls.
   - Thread-safe double-checked locking on `BigQueryClientManager.client` eliminates race conditions during lazy client initialization.
7. **Sliding-Window Rate Limiting**:
   - Built-in `RateLimitMiddleware` enforces sliding-window rate limits on the `/mcp` transport endpoint.
   - Identifies callers by Microsoft Entra ID principal identity (`sub`/`oid`/`upn`) or client IP, returning standard HTTP 429 and `Retry-After` headers while exempting health checks.

---

## File Layout

```text
bq_mcp/
├── config/
│   ├── config.yaml              # Centralized configuration (Entra ID, SA path, tools, rate limit)
│   └── settings.py              # Pydantic Settings loader for config.yaml + .env
├── src/
│   ├── __init__.py
│   ├── entra_auth.py            # Microsoft Entra ID JWT validation (JWKS + claims)
│   ├── cache.py                 # TTLCache manager with SHA-256 key hashing
│   ├── sanitizer.py             # Configurable SQL Sanitizer (Regex + AST)
│   ├── client.py                # Thread-safe BigQuery client manager & execution guardrails
│   ├── tools.py                 # Dynamic FastMCP tool definitions with error shielding
│   ├── rate_limiter.py          # Sliding-window rate limiter & Starlette middleware
│   └── server.py                # Entra ID middleware & Streamable HTTP ASGI app
├── tests/
│   ├── __init__.py
│   └── test_tools.py            # Unit & integration tests (auth, concurrency, tools, rate limit)
├── service-account.json         # BigQuery Service Account JSON key
├── .env.example                 # Optional environment overrides
├── Dockerfile                   # Hardened Python 3.12 non-root container
├── requirements.txt             # Dependency manifest
├── run.py                       # Root CLI launcher with auto-reload and options
└── README.md                    # Entra ID setup, Inspector testing, and client configs
```

---

## Configuration Specification

All operational parameters are defined in `config/config.yaml` and can be overridden via environment variables or `.env`:

```yaml
# Server & HTTP Transport Settings
server:
  host: "0.0.0.0"
  port: 8000
  endpoint_path: "/mcp"
  log_level: "INFO"

# Inbound SSO Authentication: Microsoft Entra ID (Azure AD)
security:
  enable_auth: true                      # If false, bypasses JWT validation (dev mode)
  tenant_id: "your-azure-tenant-id"      # Entra Directory (tenant) ID
  client_id: "your-app-client-id"        # Entra Application (client) ID / Audience (aud)
  jwks_cache_ttl_seconds: 86400          # 24 hours cache for Microsoft public keys
  accepted_issuers:
    - "https://login.microsoftonline.com/{tenant_id}/v2.0"
    - "https://sts.windows.net/{tenant_id}/"

# Google Cloud Service Account Authentication (Direct Backend Auth)
auth:
  service_account_key_path: "service-account.json"

# BigQuery Execution Guardrails
bigquery:
  project_id: null                       # null auto-detects from service-account.json
  location: "us-east4"
  max_rows_returned: 200                 # Maximum rows serialized to prevent OOM
  max_bytes_billed: 10737418240          # 10 GB hard cost-ceiling per query
  query_timeout_seconds: 60

# SQL Sanitizer (Enforce Read-Only Queries)
sanitizer:
  enabled: true                          # Enforce read-only checks
  mode: "both"                           # Options: "regex", "ast", "both"
  blocked_keywords:
    - "INSERT"
    - "UPDATE"
    - "DELETE"
    - "DROP"
    - "ALTER"
    - "TRUNCATE"
    - "CREATE"
    - "MERGE"
    - "GRANT"
    - "REVOKE"
    - "EXECUTE"
    - "CALL"

# Dynamic Tool Enablement Matrix
tools:
  enable_bq_list_datasets: true
  enable_bq_list_tables: true
  enable_bq_table_metadata: true
  enable_bq_query_execution: true

# In-Memory Metadata Caching (TTLCache)
cache:
  enabled: true
  metadata_ttl_seconds: 900              # 15 minutes
  max_cache_entries: 1024

# MCP Endpoint Rate Limiting Guardrails
rate_limit:
  enabled: true                          # Enable client rate limiting on MCP endpoint
  requests_per_minute: 60                # Max requests per sliding window
  window_seconds: 60                     # Sliding window duration in seconds
```

### Environment Variable Overrides (`.env`)

| Variable | Target Config | Description |
| :--- | :--- | :--- |
| `AZURE_TENANT_ID` | `security.tenant_id` | Microsoft Entra Directory (tenant) ID |
| `AZURE_CLIENT_ID` | `security.client_id` | Entra Application (client) ID |
| `SECURITY_ENABLE_AUTH` | `security.enable_auth` | Set to `false` to disable JWT check for dev mode |
| `SERVICE_ACCOUNT_KEY_PATH`| `auth.service_account_key_path`| Filepath to GCP credentials JSON |
| `BIGQUERY_PROJECT_ID` | `bigquery.project_id` | Target Google Cloud project ID |
| `BIGQUERY_LOCATION` | `bigquery.location` | Geographic dataset location (e.g. `US`, `EU`) |
| `SERVER_HOST` | `server.host` | Host binding IP (default: `0.0.0.0`) |
| `SERVER_PORT` | `server.port` | HTTP listening port (default: `8000`) |
| `SERVER_ENDPOINT_PATH` | `server.endpoint_path` | MCP transport route (default: `/mcp`) |
| `RATE_LIMIT_ENABLED` | `rate_limit.enabled` | Set to `false` to disable MCP endpoint rate limiting |
| `RATE_LIMIT_REQUESTS_PER_MINUTE` | `rate_limit.requests_per_minute` | Max requests per sliding window per client (default: `60`) |
| `RATE_LIMIT_WINDOW_SECONDS` | `rate_limit.window_seconds` | Rate limit sliding window duration in seconds (default: `60`) |

---

## Setup & Prerequisites

### 1. Microsoft Entra ID (Azure AD) Setup

1. Log in to the [Azure Portal](https://portal.azure.com/) and navigate to **Microsoft Entra ID**.
2. Go to **App registrations** > **New registration**.
   - Name: `GCP BigQuery MCP Server`
   - Supported account types: *Accounts in this organizational directory only*
3. Note down the **Application (client) ID** and **Directory (tenant) ID**.
4. Go to **Expose an API**:
   - Set the Application ID URI to `api://<client_id>`.
   - Add a scope (e.g., `BigQuery.Read`).
5. In `config/config.yaml` or `.env`, set:
   ```yaml
   security:
     tenant_id: "<your-tenant-id>"
     client_id: "<your-client-id>"
   ```

### 2. Google Cloud BigQuery Setup

1. Open [Google Cloud Console](https://console.cloud.google.com/).
2. Navigate to **IAM & Admin** > **Service Accounts** > **Create Service Account**.
3. Grant the service account the required IAM roles:
   - **BigQuery Data Viewer** (`roles/bigquery.dataViewer`): Read access to dataset tables and schemas.
   - **BigQuery Job User** (`roles/bigquery.jobUser`): Permission to submit query jobs.
4. Create and download a new **JSON key**.
5. Save the file as `service-account.json` in the root of the project (or specify the path in `config/config.yaml`).

---

## Installation & Local Execution

### 1. Create Virtual Environment and Install Dependencies

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

### 2. Run Test Suite

Run the full pytest suite covering Entra ID token validation, SQL AST sanitization, caching, and BigQuery execution:

```bash
pytest tests/ -v
```

### 3. Start the Server

You can launch the server using the `run.py` launcher:

```bash
# Default launch (reads config/config.yaml and .env)
python run.py

# Optional CLI flags
python run.py --port 8000 --host 0.0.0.0 --reload --log-level debug
```

Or via direct module invocation:
```bash
python -m src.server
```

The server starts on `http://0.0.0.0:8000` with the Streamable HTTP transport mounted at `http://0.0.0.0:8000/mcp`.

---

## Docker Deployment (Hardened Non-Root Container)

The included `Dockerfile` builds a minimal, secure container running as a non-root system user (`appuser:10001`):

```bash
# Build Docker image
docker build -t bq-mcp-server:latest .

# Run container with volume mount for service account credentials
docker run -d \
  --name bq-mcp-server \
  -p 8000:8000 \
  -v $(pwd)/service-account.json:/app/service-account.json:ro \
  -e AZURE_TENANT_ID="your-tenant-id" \
  -e AZURE_CLIENT_ID="your-client-id" \
  -e SECURITY_ENABLE_AUTH="true" \
  bq-mcp-server:latest
```

Check health status:
```bash
curl http://localhost:8000/health
```

---

## Client Integration & Verification Guide

### 1. Test via MCP Inspector

For rapid local testing and schema inspection, you can test with or without auth.

#### A. Dev Mode (Auth Disabled)
In `config/config.yaml`, set `security.enable_auth: false` (or launch with `SECURITY_ENABLE_AUTH=false`), then run:

```bash
# Terminal 1: Run the MCP Server
python -m src.server

# Terminal 2: Launch MCP Inspector
npx @modelcontextprotocol/inspector
```

- **Transport**: `Streamable HTTP`
- **URL**: `http://localhost:8000/mcp`
- Click **Connect**.

#### B. Production Mode (With Entra ID Bearer Token)
- **Transport**: `Streamable HTTP`
- **URL**: `http://localhost:8000/mcp`
- **Custom Headers**:
  ```json
  {
    "Authorization": "Bearer <YOUR_ENTRA_ACCESS_TOKEN>"
  }
  ```

### 2. Claude Desktop Integration

Add the server to your `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):

#### A. Local Development (Auth Disabled - Recommended for Quick Testing)
Set `security.enable_auth: false` in `config/config.yaml` (or `SECURITY_ENABLE_AUTH=false` in `.env`). The `--header` argument can be completely omitted:

```json
{
  "mcpServers": {
    "bigquery": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://127.0.0.1:8000/mcp"
      ]
    }
  }
}
```

#### B. Production / Authenticated Mode (With Microsoft Entra ID Token)
When `security.enable_auth: true`, obtain a token via the Azure CLI:
```bash
az login --tenant "<YOUR_AZURE_TENANT_ID>"
az account get-access-token --resource "<YOUR_AZURE_APP_CLIENT_ID>" --query accessToken -o tsv
```

Then supply the Bearer token in the `--header` argument:
```json
{
  "mcpServers": {
    "bigquery": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://127.0.0.1:8000/mcp",
        "--header",
        "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
      ]
    }
  }
}
```

### 3. IDE Integration (Cursor / VS Code / Windsurf)

In your workspace `.cursor/mcp.json` or IDE MCP settings:

#### A. Local Development (Auth Disabled)
```json
{
  "mcpServers": {
    "bigquery": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}
```

#### B. Production / Authenticated Mode
```json
{
  "mcpServers": {
    "bigquery": {
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
      }
    }
  }
}
```

---

## Exposed MCP Tools

| Tool | Parameters | Description |
| :--- | :--- | :--- |
| `bq_list_datasets` | `project_id?: str` | Lists BigQuery datasets with identifiers and labels. Cached in TTLCache. |
| `bq_list_tables` | `dataset_id: str`, `project_id?: str` | Lists tables, views, and materialized views. Cached in TTLCache. |
| `bq_table_metadata`| `dataset_id: str`, `table_id: str`, `project_id?: str` | Returns column schema, row counts, storage size, partition details, and clustering keys. Cached in TTLCache. |
| `bq_query_execution`| `query: str`, `dry_run?: bool`, `limit?: int` | Executes read-only SQL queries with AST validation, `max_bytes_billed` billing cap, and pagination. |

---

## Security Guardrails

- **AST Mutation Blocking**: Uses `sqlglot` to parse the BigQuery abstract syntax tree. Disallows query chaining (`;`), stored procedure executions (`CALL`), table drops (`DROP`), and data mutations (`INSERT`, `UPDATE`, `DELETE`, `MERGE`), including those obscured within CTEs or subqueries.
- **Cost Ceilings (`maximum_bytes_billed`)**: Protects against unexpected high-cost queries by enforcing a hard upper bound on bytes scanned.
- **Container Memory Safeguards**: Automatically sets `max_results` on BigQuery result iteration to prevent container memory exhaustion and out-of-memory crashes.
- **Principle of Least Privilege**: Inbound client auth verifies Entra ID identity, while BigQuery machine-to-machine auth is locked down via Google Cloud IAM.
# bq-mcp