#
tokens: 4524/50000 14/14 files
lines: off (toggle) GitHub
raw markdown copy
# 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 Status](https://github.com/monochromegane/mcp-bigquery-server/actions/workflows/test.yaml/badge.svg?branch=main)][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
}

```