# Directory Structure
```
├── .github
│ ├── actions
│ │ └── release
│ │ └── action.yaml
│ ├── release.yml
│ └── workflows
│ ├── release.yaml
│ ├── tagpr.yaml
│ └── test.yaml
├── .tagpr
├── bigquery_client.go
├── CHANGELOG.md
├── cli.go
├── cmd
│ └── mcp-bigquery-server
│ └── main.go
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── mcp_server_test.go
├── mcp_server.go
├── README.md
└── version.go
```
# Files
--------------------------------------------------------------------------------
/.tagpr:
--------------------------------------------------------------------------------
```
# config file for the tagpr in git config format
# The tagpr generates the initial configuration, which you can rewrite to suit your environment.
# CONFIGURATIONS:
# tagpr.releaseBranch
# Generally, it is "main." It is the branch for releases. The tagpr tracks this branch,
# creates or updates a pull request as a release candidate, or tags when they are merged.
#
# tagpr.versionFile
# Versioning file containing the semantic version needed to be updated at release.
# It will be synchronized with the "git tag".
# Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc.
# Sometimes the source code file, such as version.go or Bar.pm, is used.
# If you do not want to use versioning files but only git tags, specify the "-" string here.
# You can specify multiple version files by comma separated strings.
#
# tagpr.vPrefix
# Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true)
# This is only a tagging convention, not how it is described in the version file.
#
# tagpr.changelog (Optional)
# Flag whether or not changelog is added or changed during the release.
#
# tagpr.command (Optional)
# Command to change files just before release.
#
# tagpr.template (Optional)
# Pull request template file in go template format
#
# tagpr.templateText (Optional)
# Pull request template text in go template format
#
# tagpr.release (Optional)
# GitHub Release creation behavior after tagging [true, draft, false]
# If this value is not set, the release is to be created.
#
# tagpr.majorLabels (Optional)
# Label of major update targets. Default is [major]
#
# tagpr.minorLabels (Optional)
# Label of minor update targets. Default is [minor]
#
# tagpr.commitPrefix (Optional)
# Prefix of commit message. Default is "[tagpr]"
#
[tagpr]
vPrefix = true
releaseBranch = main
versionFile = version.go
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP BigQuery Server
[][actions]
[actions]: https://github.com/monochromegane/mcp-bigquery-server/actions?workflow=test
## Overview
MCP BigQuery Server is a server that allows you to query BigQuery tables using MCP. Written in Go, it's lightweight and easy to install with just a few commands.
## Installation
```sh
$ brew tap monochromegane/tap
$ brew install monochromegane/tap/mcp-bigquery-server
```
## Available Tools
- `list_allowed_datasets`: Get a listing of all allowed datasets.
- `list_tables`: Get a detailed listing of all tables in a specified dataset.
- `get_table_schema`: Get the schema of a specified table in a specified dataset.
- `dry_run_query`: Dry run a query to get the estimated cost and time.
## Registration
To use MCP BigQuery Server in Cursor, add the following configuration to your `.cursor/mcp.json`:
```json
{
"mcpServers": {
"BigQuery": {
"command": "mcp-bigquery-server",
"args": [
"start",
"--project",
"sample-project",
"--dataset",
"test1",
"--dataset",
"test2"
]
}
}
}
```
Note: You can specify multiple datasets by repeating the `--dataset` argument.
## License
[MIT](https://github.com/monochromegane/mcp-bigquery-server/blob/main/LICENSE)
## Author
[monochromegane](https://github.com/monochromegane)
```
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
```yaml
changelog:
exclude:
labels:
- tagpr
```
--------------------------------------------------------------------------------
/version.go:
--------------------------------------------------------------------------------
```go
package mcp_bigquery_server
const version = "0.0.8"
var revision = "HEAD"
```
--------------------------------------------------------------------------------
/mcp_server_test.go:
--------------------------------------------------------------------------------
```go
package mcp_bigquery_server
import (
"testing"
)
func TestDummy(t *testing.T) {
t.Log("dummy")
}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
```yaml
name: release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/release
with:
token: ${{ secrets.GITHUB_TOKEN }}
```
--------------------------------------------------------------------------------
/.github/actions/release/action.yaml:
--------------------------------------------------------------------------------
```yaml
name: release
inputs:
token:
description: GitHub token
required: true
runs:
using: composite
steps:
- uses: actions/setup-go@v5
with:
go-version: stable
- run: make release
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
```
--------------------------------------------------------------------------------
/cmd/mcp-bigquery-server/main.go:
--------------------------------------------------------------------------------
```go
package main
import (
"context"
"log"
cli "github.com/monochromegane/mcp-bigquery-server"
)
func main() {
ctx := context.TODO()
if err := run(ctx); err != nil {
log.Fatalf("error: %v", err)
}
}
func run(ctx context.Context) error {
c, err := cli.New()
if err != nil {
return err
}
return c.Run(ctx)
}
```
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
```yaml
name: test
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macOS-latest
- windows-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: actions/checkout@v4
- run: make test
```
--------------------------------------------------------------------------------
/.github/workflows/tagpr.yaml:
--------------------------------------------------------------------------------
```yaml
name: tagpr
on:
push:
branches: ["main"]
jobs:
tagpr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- id: tagpr
uses: Songmu/tagpr@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/release
with:
token: ${{ secrets.GITHUB_TOKEN }}
if: "steps.tagpr.outputs.tag != ''"
```
--------------------------------------------------------------------------------
/cli.go:
--------------------------------------------------------------------------------
```go
package mcp_bigquery_server
import (
"context"
"fmt"
"github.com/alecthomas/kong"
)
type CLI struct {
Version kong.VersionFlag `help:"Show version"`
Start struct {
Project string `required:"" help:"Project ID"`
Dataset []string `required:"" help:"Allowed datasets"`
} `cmd:"" help:"Start the MCP BigQuery server"`
}
func New() (*CLI, error) {
return &CLI{}, nil
}
func (c *CLI) Run(ctx context.Context) error {
k := kong.Parse(c, kong.Vars{
"version": fmt.Sprintf("%s v%s (rev:%s)", "mcp-bigquery-server", version, revision),
})
switch k.Command() {
case "start":
return StartServer(ctx, c)
}
return nil
}
```
--------------------------------------------------------------------------------
/bigquery_client.go:
--------------------------------------------------------------------------------
```go
package mcp_bigquery_server
import (
"context"
"cloud.google.com/go/bigquery"
"google.golang.org/api/iterator"
)
type BigQueryClient struct {
Project string
client *bigquery.Client
}
func NewBigQueryClient(ctx context.Context, project string) (*BigQueryClient, error) {
client, err := bigquery.NewClient(ctx, project)
if err != nil {
return nil, err
}
return &BigQueryClient{
Project: project,
client: client,
}, nil
}
func (c *BigQueryClient) ListTables(ctx context.Context, dataset string) ([]string, error) {
it := c.client.Dataset(dataset).Tables(ctx)
tables := []string{}
for {
t, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
tables = append(tables, t.TableID)
}
return tables, nil
}
func (c *BigQueryClient) GetTableSchema(ctx context.Context, dataset, table string) ([]*bigquery.FieldSchema, error) {
md, err := c.client.Dataset(dataset).Table(table).Metadata(ctx)
if err != nil {
return nil, err
}
return md.Schema, nil
}
func (c *BigQueryClient) DryRunQuery(ctx context.Context, query string, dataset string) (*bigquery.JobStatus, error) {
q := c.client.Query(query)
q.DefaultProjectID = c.Project
q.DefaultDatasetID = dataset
q.DryRun = true
job, err := q.Run(ctx)
if err != nil {
return nil, err
}
return job.LastStatus(), nil
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# Changelog
## [v0.0.8](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.7...v0.0.8) - 2025-04-09
- Update readme. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/15
- Remove unnecessary location settings. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/17
## [v0.0.7](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.6...v0.0.7) - 2025-04-08
- Add list allowed datasets. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/12
- Add dry run tool. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/14
## [v0.0.6](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.5...v0.0.6) - 2025-02-09
- Fix broken Makefile by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/10
## [v0.0.5](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.4...v0.0.5) - 2025-02-09
- Fix release action by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/8
## [v0.0.4](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.3...v0.0.4) - 2025-02-09
- Add release draft by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/6
## [v0.0.3](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.2...v0.0.3) - 2025-02-09
- Add test CI by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/4
## [v0.0.2](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.1...v0.0.2) - 2025-02-09
- Add release CI by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/3
## [v0.0.1](https://github.com/monochromegane/mcp-bigquery-server/commits/v0.0.1) - 2025-02-09
```
--------------------------------------------------------------------------------
/mcp_server.go:
--------------------------------------------------------------------------------
```go
package mcp_bigquery_server
import (
"context"
"fmt"
"log"
"slices"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type ToolName string
const (
LIST_ALLOWED_DATASETS ToolName = "list_allowed_datasets"
LIST_TABLES ToolName = "list_tables"
GET_TABLE_SCHEMA ToolName = "get_table_schema"
DRY_RUN_QUERY ToolName = "dry_run_query"
)
func StartServer(ctx context.Context, c *CLI) error {
bs, err := NewBigQueryServer(ctx, c.Start.Project, c.Start.Dataset)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
if err := bs.Serve(); err != nil {
log.Fatalf("Server error: %v", err)
}
return nil
}
func NewBigQueryServer(ctx context.Context, project string, datasets []string) (*BigQueryServer, error) {
s := &BigQueryServer{
server: server.NewMCPServer(
"bigquery-server",
version,
),
datasets: datasets,
}
client, err := NewBigQueryClient(ctx, project)
if err != nil {
return nil, err
}
s.client = client
s.server.AddTool(mcp.NewTool(string(LIST_ALLOWED_DATASETS),
mcp.WithDescription("Get a listing of all allowed datasets."),
), s.handleListAllowedDatasets)
s.server.AddTool(mcp.NewTool(string(LIST_TABLES),
mcp.WithDescription("Get a detailed listing of all tables in a specified dataset."),
mcp.WithString("dataset",
mcp.Description("The dataset to list tables from"),
mcp.Required(),
),
), s.handleListTables)
s.server.AddTool(mcp.NewTool(string(GET_TABLE_SCHEMA),
mcp.WithDescription("Get the schema of a specified table in a specified dataset."),
mcp.WithString("dataset",
mcp.Description("The dataset to get the table schema from"),
mcp.Required(),
),
mcp.WithString("table",
mcp.Description("The table to get the schema from"),
mcp.Required(),
),
), s.handleGetTableSchema)
s.server.AddTool(mcp.NewTool(string(DRY_RUN_QUERY),
mcp.WithDescription("Dry run a query to get the estimated cost and time."),
mcp.WithString("dataset",
mcp.Description("The dataset to dry run the query on"),
mcp.Required(),
),
mcp.WithString("query",
mcp.Description("The query to dry run"),
mcp.Required(),
),
), s.handleDryRunQuery)
return s, nil
}
type BigQueryServer struct {
server *server.MCPServer
client *BigQueryClient
datasets []string
}
func (s *BigQueryServer) Serve() error {
return server.ServeStdio(s.server)
}
func (s *BigQueryServer) handleListAllowedDatasets(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
return &mcp.CallToolResult{
Content: []interface{}{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("Allowed datasets: %s", strings.Join(s.datasets, ", ")),
},
},
}, nil
}
func (s *BigQueryServer) handleListTables(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
dataset, ok := arguments["dataset"].(string)
if !ok {
return nil, fmt.Errorf("dataset must be a string")
}
if !slices.Contains(s.datasets, dataset) {
return nil, fmt.Errorf("dataset %s not allowed", dataset)
}
tables, err := s.client.ListTables(ctx, dataset)
if err != nil {
return nil, err
}
var tablesStr string
for _, table := range tables {
tablesStr += fmt.Sprintf("- %s\n", table)
}
return &mcp.CallToolResult{
Content: []interface{}{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("Tables in dataset `%s`:\n\n%s", dataset, tablesStr),
},
},
}, nil
}
func (s *BigQueryServer) handleGetTableSchema(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
dataset, ok := arguments["dataset"].(string)
if !ok {
return nil, fmt.Errorf("dataset must be a string")
}
if !slices.Contains(s.datasets, dataset) {
return nil, fmt.Errorf("dataset %s not allowed", dataset)
}
table, ok := arguments["table"].(string)
if !ok {
return nil, fmt.Errorf("table must be a string")
}
schema, err := s.client.GetTableSchema(ctx, dataset, table)
if err != nil {
return nil, err
}
var schemaStr string
for _, field := range schema {
schemaStr += fmt.Sprintf("- %s (%s)\n", field.Name, field.Type)
if field.Description != "" {
schemaStr += fmt.Sprintf(" Description: %s\n", field.Description)
}
if field.Repeated {
schemaStr += " Repeated: true\n"
}
if field.Required {
schemaStr += " Required: true\n"
}
schemaStr += "\n"
}
return &mcp.CallToolResult{
Content: []interface{}{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("Schema for table %s in dataset %s:\n\n%s", table, dataset, schemaStr),
},
},
}, nil
}
func (s *BigQueryServer) handleDryRunQuery(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
dataset, ok := arguments["dataset"].(string)
if !ok {
return nil, fmt.Errorf("dataset must be a string")
}
if !slices.Contains(s.datasets, dataset) {
return nil, fmt.Errorf("dataset %s not allowed", dataset)
}
query, ok := arguments["query"].(string)
if !ok {
return nil, fmt.Errorf("query must be a string")
}
status, err := s.client.DryRunQuery(ctx, query, dataset)
if err != nil {
return nil, err
}
errors := status.Errors
totalBytesProcessed := status.Statistics.TotalBytesProcessed
return &mcp.CallToolResult{
Content: []interface{}{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("Errors: %v\nTotal bytes processed: %d", errors, totalBytesProcessed),
},
},
}, nil
}
```