Skip to content

Commit 47dc379

Browse files
author
Lucas Hild
committed
Initial commit
0 parents  commit 47dc379

File tree

7 files changed

+856
-0
lines changed

7 files changed

+856
-0
lines changed

.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv

.python-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

README.md

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# BigQuery MCP server
2+
3+
A Model Context Protocol server that provides access to BigQuery. This server enables LLMs to inspect database schemas and execute queries.
4+
5+
## Components
6+
7+
### Tools
8+
9+
The server implements one tool:
10+
11+
- `execute-query`: Executes a SQL query using BigQuery dialect
12+
- `list-tables`: Lists all tables in the BigQuery database
13+
- `describe-table`: Describes the schema of a specific table
14+
15+
## Configuration
16+
17+
The server can be configured with the following arguments:
18+
19+
- `--project` (required): The GCP project ID.
20+
- `--location` (required): The GCP location (e.g. `europe-west9`).
21+
- `--dataset` (optional): Only take specific BigQuery datasets into consideration. Several datasets can be specified by repeating the argument (e.g. `--dataset my_dataset_1 --dataset my_dataset_2`). If not provided, all tables in the project will be considered.
22+
23+
## Quickstart
24+
25+
### Install
26+
27+
#### Claude Desktop
28+
29+
On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
30+
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
31+
32+
<details>
33+
<summary>Development/Unpublished Servers Configuration</summary>
34+
```
35+
"mcpServers": {
36+
"bigquery": {
37+
"command": "uv",
38+
"args": [
39+
"--directory",
40+
"{{PATH_TO_REPO}}",
41+
"run",
42+
"mcp-server-bigquery",
43+
"--project",
44+
"{{GCP_PROJECT_ID}}",
45+
"--location",
46+
"{{GCP_LOCATION}}"
47+
]
48+
}
49+
}
50+
```
51+
</details>
52+
53+
<details>
54+
<summary>Published Servers Configuration</summary>
55+
```
56+
"mcpServers": {
57+
"bigquery": {
58+
"command": "uvx",
59+
"args": [
60+
"mcp-server-bigquery",
61+
"--project",
62+
"{{GCP_PROJECT_ID}}",
63+
"--location",
64+
"{{GCP_LOCATION}}"
65+
]
66+
}
67+
}
68+
```
69+
</details>
70+
71+
Replace `{{PATH_TO_REPO}}`, `{{GCP_PROJECT_ID}}`, and `{{GCP_LOCATION}}` with the appropriate values.
72+
73+
## Development
74+
75+
### Building and Publishing
76+
77+
To prepare the package for distribution:
78+
79+
1. Sync dependencies and update lockfile:
80+
81+
```bash
82+
uv sync
83+
```
84+
85+
2. Build package distributions:
86+
87+
```bash
88+
uv build
89+
```
90+
91+
This will create source and wheel distributions in the `dist/` directory.
92+
93+
3. Publish to PyPI:
94+
95+
```bash
96+
uv publish
97+
```
98+
99+
Note: You'll need to set PyPI credentials via environment variables or command flags:
100+
101+
- Token: `--token` or `UV_PUBLISH_TOKEN`
102+
- Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
103+
104+
### Debugging
105+
106+
Since MCP servers run over stdio, debugging can be challenging. For the best debugging
107+
experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
108+
109+
You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
110+
111+
```bash
112+
npx @modelcontextprotocol/inspector uv --directory {{PATH_TO_REPO}} run mcp-server-bigquery
113+
```
114+
115+
Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.

pyproject.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[project]
2+
name = "mcp-server-bigquery"
3+
version = "0.2.0"
4+
description = "A Model Context Protocol server that provides access to BigQuery. This server enables LLMs to inspect database schemas and execute queries."
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"google-cloud-bigquery>=3.27.0",
9+
"mcp>=1.0.0",
10+
]
11+
[[project.authors]]
12+
name = "Lucas Hild"
13+
email = ""
14+
15+
[build-system]
16+
requires = [ "hatchling",]
17+
build-backend = "hatchling.build"
18+
19+
[project.scripts]
20+
mcp-server-bigquery = "mcp_server_bigquery:main"

src/mcp_server_bigquery/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from . import server
2+
import asyncio
3+
import argparse
4+
def main():
5+
"""Main entry point for the package."""
6+
parser = argparse.ArgumentParser(description='BigQuery MCP Server')
7+
parser.add_argument('--project', help='BigQuery project', required=False)
8+
parser.add_argument('--location', help='BigQuery location', required=False)
9+
parser.add_argument('--dataset', help='BigQuery dataset', required=False, action='append')
10+
11+
args = parser.parse_args()
12+
13+
datasets_filter = args.dataset if args.dataset else []
14+
asyncio.run(server.main(args.project, args.location, datasets_filter))
15+
16+
# Optionally expose other important items at package level
17+
__all__ = ['main', 'server']

src/mcp_server_bigquery/server.py

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from google.cloud import bigquery
2+
import logging
3+
from mcp.server.models import InitializationOptions
4+
import mcp.types as types
5+
from mcp.server import NotificationOptions, Server
6+
import mcp.server.stdio
7+
from typing import Any
8+
9+
# Set up logging to both stdout and file
10+
logger = logging.getLogger('mcp_bigquery_server')
11+
handler_stdout = logging.StreamHandler()
12+
handler_file = logging.FileHandler('/tmp/mcp_bigquery_server.log')
13+
14+
# Set format for both handlers
15+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
16+
handler_stdout.setFormatter(formatter)
17+
handler_file.setFormatter(formatter)
18+
19+
# Add both handlers to logger
20+
logger.addHandler(handler_stdout)
21+
logger.addHandler(handler_file)
22+
23+
# Set overall logging level
24+
logger.setLevel(logging.DEBUG)
25+
26+
logger.info("Starting MCP BigQuery Server")
27+
28+
class BigQueryDatabase:
29+
def __init__(self, project: str, location: str, datasets_filter: list[str]):
30+
"""Initialize a BigQuery database client"""
31+
if not project:
32+
raise ValueError("Project is required")
33+
if not location:
34+
raise ValueError("Location is required")
35+
36+
self.client = bigquery.Client(project=project, location=location)
37+
self.datasets_filter = datasets_filter
38+
39+
def execute_query(self, query: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
40+
"""Execute a SQL query and return results as a list of dictionaries"""
41+
logger.debug(f"Executing query: {query}")
42+
try:
43+
if params:
44+
job = self.client.query(query, job_config=bigquery.QueryJobConfig(query_parameters=params))
45+
else:
46+
job = self.client.query(query)
47+
48+
results = job.result()
49+
rows = [dict(row.items()) for row in results]
50+
logger.debug(f"Query returned {len(rows)} rows")
51+
return rows
52+
except Exception as e:
53+
logger.error(f"Database error executing query: {e}")
54+
raise
55+
56+
def list_tables(self) -> list[str]:
57+
"""List all tables in the BigQuery database"""
58+
logger.debug("Listing all tables")
59+
60+
if self.datasets_filter:
61+
datasets = [self.client.dataset(dataset) for dataset in self.datasets_filter]
62+
else:
63+
datasets = list(self.client.list_datasets())
64+
65+
logger.debug(f"Found {len(datasets)} datasets")
66+
67+
tables = []
68+
for dataset in datasets:
69+
dataset_tables = self.client.list_tables(dataset.dataset_id)
70+
tables.extend([
71+
f"{dataset.dataset_id}.{table.table_id}" for table in dataset_tables
72+
])
73+
74+
logger.debug(f"Found {len(tables)} tables")
75+
return tables
76+
77+
def describe_table(self, table_name: str) -> list[dict[str, Any]]:
78+
"""Describe a table in the BigQuery database"""
79+
logger.debug(f"Describing table: {table_name}")
80+
81+
parts = table_name.split(".")
82+
if len(parts) != 2:
83+
raise ValueError(f"Invalid table name: {table_name}")
84+
85+
dataset_id = parts[0]
86+
table_id = parts[1]
87+
88+
query = f"""
89+
SELECT ddl
90+
FROM {dataset_id}.INFORMATION_SCHEMA.TABLES
91+
WHERE table_name = @table_name;
92+
"""
93+
return self.execute_query(query, params=[
94+
bigquery.ScalarQueryParameter("table_name", "STRING", table_id),
95+
])
96+
97+
async def main(project: str, location: str, datasets_filter: list[str]):
98+
logger.info(f"Starting BigQuery MCP Server with project: {project} and location: {location}")
99+
100+
db = BigQueryDatabase(project, location, datasets_filter)
101+
server = Server("bigquery-manager")
102+
103+
# Register handlers
104+
logger.debug("Registering handlers")
105+
106+
@server.list_tools()
107+
async def handle_list_tools() -> list[types.Tool]:
108+
"""List available tools"""
109+
return [
110+
types.Tool(
111+
name="execute-query",
112+
description="Execute a SELECT query on the BigQuery database",
113+
inputSchema={
114+
"type": "object",
115+
"properties": {
116+
"query": {"type": "string", "description": "SELECT SQL query to execute using BigQuery dialect"},
117+
},
118+
"required": ["query"],
119+
},
120+
),
121+
types.Tool(
122+
name="list-tables",
123+
description="List all tables in the BigQuery database",
124+
inputSchema={
125+
"type": "object",
126+
"properties": {},
127+
},
128+
),
129+
types.Tool(
130+
name="describe-table",
131+
description="Get the schema information for a specific table",
132+
inputSchema={
133+
"type": "object",
134+
"properties": {
135+
"table_name": {"type": "string", "description": "Name of the table to describe (e.g. my_dataset.my_table)"},
136+
},
137+
"required": ["table_name"],
138+
},
139+
),
140+
]
141+
142+
@server.call_tool()
143+
async def handle_call_tool(
144+
name: str, arguments: dict[str, Any] | None
145+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
146+
"""Handle tool execution requests"""
147+
logger.debug(f"Handling tool execution request: {name}")
148+
149+
try:
150+
if name == "list-tables":
151+
results = db.list_tables()
152+
return [types.TextContent(type="text", text=str(results))]
153+
154+
elif name == "describe-table":
155+
if not arguments or "table_name" not in arguments:
156+
raise ValueError("Missing table_name argument")
157+
results = db.describe_table(arguments["table_name"])
158+
return [types.TextContent(type="text", text=str(results))]
159+
160+
if name == "execute-query":
161+
results = db.execute_query(arguments["query"])
162+
return [types.TextContent(type="text", text=str(results))]
163+
164+
else:
165+
raise ValueError(f"Unknown tool: {name}")
166+
except Exception as e:
167+
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
168+
169+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
170+
logger.info("Server running with stdio transport")
171+
await server.run(
172+
read_stream,
173+
write_stream,
174+
InitializationOptions(
175+
server_name="bigquery",
176+
server_version="0.2.0",
177+
capabilities=server.get_capabilities(
178+
notification_options=NotificationOptions(),
179+
experimental_capabilities={},
180+
),
181+
),
182+
)

0 commit comments

Comments
 (0)