Secure BigQuery MCP Gateway
Enables AI assistants to query BigQuery read-only via MCP with enforced dataset boundaries, result limits, and audit labels.
README
Secure BigQuery MCP Gateway
A public reference implementation for connecting an AI assistant to BigQuery without giving the assistant a Google credential. It runs a remote MCP server on Cloud Run and enforces read-only access, dataset boundaries, result limits, query budget limits, and audit-friendly BigQuery labels.
Portfolio project, not a client system. It contains no customer data, project IDs, service-account keys, or production endpoints.
The design problem
An AI assistant needs to answer questions from an analytics warehouse while a business owner is offline. The common mistake is to reuse a person's Google OAuth refresh token for every part of the connection. This creates a fragile and over-privileged integration.
This project splits the trust boundary:
AI assistant / scheduled workflow
└── authenticates to the MCP endpoint
└── Cloud Run MCP gateway
└── uses its own restricted service account for BigQuery
The caller can request an approved tool. It never receives direct Google Cloud credentials. The Cloud Run service identity can query only approved reporting data. It cannot modify the warehouse.
What it demonstrates
- Remote MCP over Streamable HTTP, the recommended transport for deployed MCP servers.
- Python MCP implementation with a Cloud Run-ready container.
- Separate inbound caller authentication and outbound Google service identity.
- OAuth/OIDC bearer-token verification with JWKS caching, falling back to a static bearer
token for simple machine-to-machine callers — see
identity.py. - Role-based access control: each verified identity's role(s) resolve to a set of reachable
datasets/schemas via
ROLE_DATASET_MAP, enforced independently of, and in addition to, the gateway-wide allowlist. - A second, independent read-only connector for Postgres (schema allowlist,
SET TRANSACTION READ ONLY, statement timeout, row cap) — seepostgres_service.py. Disabled by default; enabling it is one environment variable. - A host-allowlisted REST connector for calling an approved external API without ever handing the caller, or the model, that API's credential.
- Best-effort PII masking applied to every connector's result rows before they leave the gateway (email/phone/SSN/card-shaped substrings) — a defence-in-depth layer, not a substitute for querying pre-masked views.
- Structured JSON audit logging of every tool call (subject, role, tool, decision, latency, row count, masked-field count) — one Cloud Logging entry per call, no raw query text or row contents logged.
- Per-identity rate limiting and a rolling daily byte budget, enforced in memory.
- Application Default Credentials rather than a service-account JSON key.
- BigQuery dry-run cost checks plus
maximum_bytes_billedenforcement. - SQL guardrails: one statement,
SELECT/WITH ... SELECTonly, dataset allowlist, row cap, timeout, and query labels. - A production-minded recommendation to grant access to curated reporting views rather than raw operational tables.
See the architecture decision record for the security rationale and the threat model for what each control does and doesn't cover.
Inbound authentication: bearer token or OIDC
The reference build defaults to a bearer token injected from Secret Manager — appropriate for a
controlled machine-to-machine scheduler, and it makes the identity separation visible in a small
project. Setting OIDC_ISSUER, OIDC_AUDIENCE, and OIDC_JWKS_URL switches the same deployment
to standards-compliant OIDC token verification with no code change: identity.py resolves roles
from the configured claim (OIDC_ROLE_CLAIM, default roles) and RBAC/audit logging use the
verified subject and roles either way. BigQuery access remains unchanged in both modes because it
always comes from the Cloud Run service account, never from the caller's token.
Local development
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env
# Update .env with a non-production project, allowed reporting dataset, and random token.
python -m secure_bigquery_mcp_gateway.app
The MCP endpoint is available at http://localhost:8080/mcp. Supply:
Authorization: Bearer <MCP_BEARER_TOKEN>
or, once OIDC_ISSUER/OIDC_AUDIENCE/OIDC_JWKS_URL are set, a valid OIDC identity token from
that issuer.
Four tools are exposed: execute_readonly_sql (BigQuery), execute_readonly_sql_postgres
(Postgres, only when POSTGRES_DSN is set), call_allowed_rest_api (only hosts in
REST_ALLOWED_HOSTS), and gateway_capabilities (reports the caller's own resolved roles and
reachable datasets — useful for confirming an RBAC change took effect without guessing).
Run the tests:
pytest -q
# or, without pytest:
python -m unittest discover -s tests
Deploy to Cloud Run
Before deploying, create a dedicated service account and grant:
roles/bigquery.jobUseron the query project.roles/bigquery.dataVieweron only the approved dataset or, preferably, curated views.
Build and deploy from the client-owned Google Cloud project:
gcloud builds submit --tag europe-west2-docker.pkg.dev/PROJECT_ID/mcp/secure-bigquery-mcp-gateway
gcloud run deploy secure-bigquery-mcp-gateway \
--image europe-west2-docker.pkg.dev/PROJECT_ID/mcp/secure-bigquery-mcp-gateway \
--region europe-west2 \
--service-account claude-bq-mcp@PROJECT_ID.iam.gserviceaccount.com \
--allow-unauthenticated \
--set-env-vars GOOGLE_CLOUD_PROJECT=PROJECT_ID,ALLOWED_DATASETS=analytics_reporting \
--set-secrets MCP_BEARER_TOKEN=mcp-bearer-token:latest
The application needs no GOOGLE_APPLICATION_CREDENTIALS file. Cloud Run supplies short-lived
credentials for its attached service account automatically.
--allow-unauthenticated is intentional for this reference build: the remote MCP caller cannot
usually mint a Google Cloud IAM token. The application protects /mcp with its own bearer-token
layer, which is separate from the Cloud Run service identity used for BigQuery. For an OAuth/OIDC
capable MCP client, replace this with a standards-compliant verifier and add edge protection.
Production checklist
- [ ] Use reporting views or an authorised view layer, not raw customer/event tables.
- [ ] Prefer OIDC over the static bearer token once the MCP client supports it; rotate whichever is active.
- [ ] Set a conservative byte cap and alert on BigQuery job labels.
- [ ] Move
rate_limit.py's in-memory state to Memorystore/Redis before running more than one instance. - [ ] Restrict Cloud Run ingress and use a custom domain/WAF when appropriate.
- [ ] Point
ROLE_DATASET_MAPat the real roles your OIDC issuer will send, not just the referenceservice:*. - [ ] If enabling Postgres, confirm the connecting database role is itself granted
SELECTonly — this gateway's read-only transaction is a second layer, not a substitute for that grant. - [ ] Review
REST_ALLOWED_HOSTSagainst the actual integrations you intend to expose; an empty allowlist (the default) means the REST tool can reach nothing. - [ ] Confirm
mask_piibehaves as expected against a sample of real column values before relying on it. - [ ] Test a cold start, a denied write query, an over-budget query, an RBAC-denied dataset, and the real overnight caller.
- [ ] Keep deployment privileges separate from the running service account.
Project story for clients
I designed this gateway to solve the gap between an AI assistant's ability to call tools and a production data warehouse's need for least-privilege access. The key decision is separating the assistant's authentication to the MCP endpoint from the Cloud Run service account that executes constrained, read-only BigQuery queries. I extended it from a single-connector, bearer-token-only prototype into a multi-connector gateway (BigQuery, Postgres, and an allowlisted REST API) with OIDC support, per-role dataset access, PII-aware result masking, structured audit logging, and per-identity rate limits — the shape a client would actually need before trusting it with more than one data source or more than one caller.
Known issue worth knowing about
mcp>=2.0.0 removed mcp.server.fastmcp, which this project's server is built on. pyproject.toml
pins mcp<2.0.0 accordingly — if a dependency bump ever breaks this, that's why.
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。