#
tokens: 6104/50000 14/14 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # config file for the tagpr in git config format
 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment.
 3 | # CONFIGURATIONS:
 4 | #   tagpr.releaseBranch
 5 | #       Generally, it is "main." It is the branch for releases. The tagpr tracks this branch,
 6 | #       creates or updates a pull request as a release candidate, or tags when they are merged.
 7 | #
 8 | #   tagpr.versionFile
 9 | #       Versioning file containing the semantic version needed to be updated at release.
10 | #       It will be synchronized with the "git tag".
11 | #       Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc.
12 | #       Sometimes the source code file, such as version.go or Bar.pm, is used.
13 | #       If you do not want to use versioning files but only git tags, specify the "-" string here.
14 | #       You can specify multiple version files by comma separated strings.
15 | #
16 | #   tagpr.vPrefix
17 | #       Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true)
18 | #       This is only a tagging convention, not how it is described in the version file.
19 | #
20 | #   tagpr.changelog (Optional)
21 | #       Flag whether or not changelog is added or changed during the release.
22 | #
23 | #   tagpr.command (Optional)
24 | #       Command to change files just before release.
25 | #
26 | #   tagpr.template (Optional)
27 | #       Pull request template file in go template format
28 | #
29 | #   tagpr.templateText (Optional)
30 | #       Pull request template text in go template format
31 | #
32 | #   tagpr.release (Optional)
33 | #       GitHub Release creation behavior after tagging [true, draft, false]
34 | #       If this value is not set, the release is to be created.
35 | #
36 | #   tagpr.majorLabels (Optional)
37 | #       Label of major update targets. Default is [major]
38 | #
39 | #   tagpr.minorLabels (Optional)
40 | #       Label of minor update targets. Default is [minor]
41 | #
42 | #   tagpr.commitPrefix (Optional)
43 | #       Prefix of commit message. Default is "[tagpr]"
44 | #
45 | [tagpr]
46 | 	vPrefix = true
47 | 	releaseBranch = main
48 | 	versionFile = version.go
49 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # MCP BigQuery Server
 2 | 
 3 | [![Actions Status](https://github.com/monochromegane/mcp-bigquery-server/actions/workflows/test.yaml/badge.svg?branch=main)][actions]
 4 | 
 5 | [actions]: https://github.com/monochromegane/mcp-bigquery-server/actions?workflow=test
 6 | 
 7 | ## Overview
 8 | 
 9 | 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.
10 | 
11 | ## Installation
12 | 
13 | ```sh
14 | $ brew tap monochromegane/tap
15 | $ brew install monochromegane/tap/mcp-bigquery-server
16 | ```
17 | 
18 | ## Available Tools
19 | 
20 | - `list_allowed_datasets`: Get a listing of all allowed datasets.
21 | - `list_tables`: Get a detailed listing of all tables in a specified dataset.
22 | - `get_table_schema`: Get the schema of a specified table in a specified dataset.
23 | - `dry_run_query`: Dry run a query to get the estimated cost and time.
24 | 
25 | ## Registration
26 | 
27 | To use MCP BigQuery Server in Cursor, add the following configuration to your `.cursor/mcp.json`:
28 | 
29 | ```json
30 | {
31 |   "mcpServers": {
32 |     "BigQuery": {
33 |       "command": "mcp-bigquery-server",
34 |       "args": [
35 |         "start",
36 |         "--project",
37 |         "sample-project",
38 |         "--dataset",
39 |         "test1",
40 |         "--dataset",
41 |         "test2"
42 |       ]
43 |     }
44 |   }
45 | }
46 | ```
47 | 
48 | Note: You can specify multiple datasets by repeating the `--dataset` argument.
49 | 
50 | ## License
51 | 
52 | [MIT](https://github.com/monochromegane/mcp-bigquery-server/blob/main/LICENSE)
53 | 
54 | ## Author
55 | 
56 | [monochromegane](https://github.com/monochromegane)
```

--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------

```yaml
1 | changelog:
2 |   exclude:
3 |     labels:
4 |       - tagpr
5 | 
```

--------------------------------------------------------------------------------
/version.go:
--------------------------------------------------------------------------------

```go
1 | package mcp_bigquery_server
2 | 
3 | const version = "0.0.8"
4 | 
5 | var revision = "HEAD"
6 | 
```

--------------------------------------------------------------------------------
/mcp_server_test.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp_bigquery_server
 2 | 
 3 | import (
 4 | 	"testing"
 5 | )
 6 | 
 7 | func TestDummy(t *testing.T) {
 8 | 	t.Log("dummy")
 9 | }
10 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: release
 2 | on:
 3 |   push:
 4 |     tags:
 5 |     - "v[0-9]+.[0-9]+.[0-9]+"
 6 | jobs:
 7 |   release:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |     - uses: actions/checkout@v4
11 |     - uses: ./.github/actions/release
12 |       with:
13 |         token: ${{ secrets.GITHUB_TOKEN }}
14 | 
```

--------------------------------------------------------------------------------
/.github/actions/release/action.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: release
 2 | inputs:
 3 |   token:
 4 |     description: GitHub token
 5 |     required: true
 6 | runs:
 7 |   using: composite
 8 |   steps:
 9 |   - uses: actions/setup-go@v5
10 |     with:
11 |       go-version: stable
12 |   - run: make release
13 |     shell: bash
14 |     env:
15 |       GITHUB_TOKEN: ${{ inputs.token }}
16 | 
```

--------------------------------------------------------------------------------
/cmd/mcp-bigquery-server/main.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"log"
 6 | 
 7 | 	cli "github.com/monochromegane/mcp-bigquery-server"
 8 | )
 9 | 
10 | func main() {
11 | 	ctx := context.TODO()
12 | 	if err := run(ctx); err != nil {
13 | 		log.Fatalf("error: %v", err)
14 | 	}
15 | }
16 | 
17 | func run(ctx context.Context) error {
18 | 	c, err := cli.New()
19 | 	if err != nil {
20 | 		return err
21 | 	}
22 | 
23 | 	return c.Run(ctx)
24 | }
25 | 
```

--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: test
 2 | on:
 3 |   push:
 4 |     branches:
 5 |       - main
 6 |   pull_request:
 7 | jobs:
 8 |   test:
 9 |     runs-on: ${{ matrix.os }}
10 |     strategy:
11 |       fail-fast: false
12 |       matrix:
13 |         os:
14 |         - ubuntu-latest
15 |         - macOS-latest
16 |         - windows-latest
17 |     steps:
18 |     - uses: actions/setup-go@v5
19 |       with:
20 |         go-version: stable
21 |     - uses: actions/checkout@v4
22 |     - run: make test
23 | 
```

--------------------------------------------------------------------------------
/.github/workflows/tagpr.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: tagpr
 2 | on:
 3 |   push:
 4 |     branches: ["main"]
 5 | jobs:
 6 |   tagpr:
 7 |     runs-on: ubuntu-latest
 8 |     permissions:
 9 |       contents: write
10 |       pull-requests: write
11 |     steps:
12 |     - uses: actions/checkout@v4
13 |     - id: tagpr
14 |       uses: Songmu/tagpr@v1
15 |       env:
16 |         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 |     - uses: ./.github/actions/release
18 |       with:
19 |         token: ${{ secrets.GITHUB_TOKEN }}
20 |       if: "steps.tagpr.outputs.tag != ''"
21 | 
```

--------------------------------------------------------------------------------
/cli.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp_bigquery_server
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 
 7 | 	"github.com/alecthomas/kong"
 8 | )
 9 | 
10 | type CLI struct {
11 | 	Version kong.VersionFlag `help:"Show version"`
12 | 	Start   struct {
13 | 		Project string   `required:"" help:"Project ID"`
14 | 		Dataset []string `required:"" help:"Allowed datasets"`
15 | 	} `cmd:"" help:"Start the MCP BigQuery server"`
16 | }
17 | 
18 | func New() (*CLI, error) {
19 | 	return &CLI{}, nil
20 | }
21 | 
22 | func (c *CLI) Run(ctx context.Context) error {
23 | 	k := kong.Parse(c, kong.Vars{
24 | 		"version": fmt.Sprintf("%s v%s (rev:%s)", "mcp-bigquery-server", version, revision),
25 | 	})
26 | 
27 | 	switch k.Command() {
28 | 	case "start":
29 | 		return StartServer(ctx, c)
30 | 	}
31 | 	return nil
32 | }
33 | 
```

--------------------------------------------------------------------------------
/bigquery_client.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp_bigquery_server
 2 | 
 3 | import (
 4 | 	"context"
 5 | 
 6 | 	"cloud.google.com/go/bigquery"
 7 | 	"google.golang.org/api/iterator"
 8 | )
 9 | 
10 | type BigQueryClient struct {
11 | 	Project string
12 | 	client  *bigquery.Client
13 | }
14 | 
15 | func NewBigQueryClient(ctx context.Context, project string) (*BigQueryClient, error) {
16 | 	client, err := bigquery.NewClient(ctx, project)
17 | 	if err != nil {
18 | 		return nil, err
19 | 	}
20 | 	return &BigQueryClient{
21 | 		Project: project,
22 | 		client:  client,
23 | 	}, nil
24 | }
25 | 
26 | func (c *BigQueryClient) ListTables(ctx context.Context, dataset string) ([]string, error) {
27 | 	it := c.client.Dataset(dataset).Tables(ctx)
28 | 	tables := []string{}
29 | 	for {
30 | 		t, err := it.Next()
31 | 		if err == iterator.Done {
32 | 			break
33 | 		}
34 | 		if err != nil {
35 | 			return nil, err
36 | 		}
37 | 		tables = append(tables, t.TableID)
38 | 	}
39 | 	return tables, nil
40 | }
41 | 
42 | func (c *BigQueryClient) GetTableSchema(ctx context.Context, dataset, table string) ([]*bigquery.FieldSchema, error) {
43 | 	md, err := c.client.Dataset(dataset).Table(table).Metadata(ctx)
44 | 	if err != nil {
45 | 		return nil, err
46 | 	}
47 | 	return md.Schema, nil
48 | }
49 | 
50 | func (c *BigQueryClient) DryRunQuery(ctx context.Context, query string, dataset string) (*bigquery.JobStatus, error) {
51 | 	q := c.client.Query(query)
52 | 	q.DefaultProjectID = c.Project
53 | 	q.DefaultDatasetID = dataset
54 | 	q.DryRun = true
55 | 	job, err := q.Run(ctx)
56 | 	if err != nil {
57 | 		return nil, err
58 | 	}
59 | 	return job.LastStatus(), nil
60 | }
61 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | ## [v0.0.8](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.7...v0.0.8) - 2025-04-09
 4 | - Update readme. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/15
 5 | - Remove unnecessary location settings. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/17
 6 | 
 7 | ## [v0.0.7](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.6...v0.0.7) - 2025-04-08
 8 | - Add list allowed datasets. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/12
 9 | - Add dry run tool. by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/14
10 | 
11 | ## [v0.0.6](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.5...v0.0.6) - 2025-02-09
12 | - Fix broken Makefile by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/10
13 | 
14 | ## [v0.0.5](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.4...v0.0.5) - 2025-02-09
15 | - Fix release action by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/8
16 | 
17 | ## [v0.0.4](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.3...v0.0.4) - 2025-02-09
18 | - Add release draft by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/6
19 | 
20 | ## [v0.0.3](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.2...v0.0.3) - 2025-02-09
21 | - Add test CI by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/4
22 | 
23 | ## [v0.0.2](https://github.com/monochromegane/mcp-bigquery-server/compare/v0.0.1...v0.0.2) - 2025-02-09
24 | - Add release CI by @monochromegane in https://github.com/monochromegane/mcp-bigquery-server/pull/3
25 | 
26 | ## [v0.0.1](https://github.com/monochromegane/mcp-bigquery-server/commits/v0.0.1) - 2025-02-09
27 | 
```

--------------------------------------------------------------------------------
/mcp_server.go:
--------------------------------------------------------------------------------

```go
  1 | package mcp_bigquery_server
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"log"
  7 | 	"slices"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | 	"github.com/mark3labs/mcp-go/server"
 12 | )
 13 | 
 14 | type ToolName string
 15 | 
 16 | const (
 17 | 	LIST_ALLOWED_DATASETS ToolName = "list_allowed_datasets"
 18 | 	LIST_TABLES           ToolName = "list_tables"
 19 | 	GET_TABLE_SCHEMA      ToolName = "get_table_schema"
 20 | 	DRY_RUN_QUERY         ToolName = "dry_run_query"
 21 | )
 22 | 
 23 | func StartServer(ctx context.Context, c *CLI) error {
 24 | 	bs, err := NewBigQueryServer(ctx, c.Start.Project, c.Start.Dataset)
 25 | 	if err != nil {
 26 | 		log.Fatalf("Failed to create server: %v", err)
 27 | 	}
 28 | 
 29 | 	if err := bs.Serve(); err != nil {
 30 | 		log.Fatalf("Server error: %v", err)
 31 | 	}
 32 | 	return nil
 33 | }
 34 | 
 35 | func NewBigQueryServer(ctx context.Context, project string, datasets []string) (*BigQueryServer, error) {
 36 | 	s := &BigQueryServer{
 37 | 		server: server.NewMCPServer(
 38 | 			"bigquery-server",
 39 | 			version,
 40 | 		),
 41 | 		datasets: datasets,
 42 | 	}
 43 | 
 44 | 	client, err := NewBigQueryClient(ctx, project)
 45 | 	if err != nil {
 46 | 		return nil, err
 47 | 	}
 48 | 	s.client = client
 49 | 
 50 | 	s.server.AddTool(mcp.NewTool(string(LIST_ALLOWED_DATASETS),
 51 | 		mcp.WithDescription("Get a listing of all allowed datasets."),
 52 | 	), s.handleListAllowedDatasets)
 53 | 
 54 | 	s.server.AddTool(mcp.NewTool(string(LIST_TABLES),
 55 | 		mcp.WithDescription("Get a detailed listing of all tables in a specified dataset."),
 56 | 		mcp.WithString("dataset",
 57 | 			mcp.Description("The dataset to list tables from"),
 58 | 			mcp.Required(),
 59 | 		),
 60 | 	), s.handleListTables)
 61 | 
 62 | 	s.server.AddTool(mcp.NewTool(string(GET_TABLE_SCHEMA),
 63 | 		mcp.WithDescription("Get the schema of a specified table in a specified dataset."),
 64 | 		mcp.WithString("dataset",
 65 | 			mcp.Description("The dataset to get the table schema from"),
 66 | 			mcp.Required(),
 67 | 		),
 68 | 		mcp.WithString("table",
 69 | 			mcp.Description("The table to get the schema from"),
 70 | 			mcp.Required(),
 71 | 		),
 72 | 	), s.handleGetTableSchema)
 73 | 
 74 | 	s.server.AddTool(mcp.NewTool(string(DRY_RUN_QUERY),
 75 | 		mcp.WithDescription("Dry run a query to get the estimated cost and time."),
 76 | 		mcp.WithString("dataset",
 77 | 			mcp.Description("The dataset to dry run the query on"),
 78 | 			mcp.Required(),
 79 | 		),
 80 | 		mcp.WithString("query",
 81 | 			mcp.Description("The query to dry run"),
 82 | 			mcp.Required(),
 83 | 		),
 84 | 	), s.handleDryRunQuery)
 85 | 
 86 | 	return s, nil
 87 | }
 88 | 
 89 | type BigQueryServer struct {
 90 | 	server   *server.MCPServer
 91 | 	client   *BigQueryClient
 92 | 	datasets []string
 93 | }
 94 | 
 95 | func (s *BigQueryServer) Serve() error {
 96 | 	return server.ServeStdio(s.server)
 97 | }
 98 | 
 99 | func (s *BigQueryServer) handleListAllowedDatasets(
100 | 	ctx context.Context,
101 | 	request mcp.CallToolRequest,
102 | ) (*mcp.CallToolResult, error) {
103 | 	return &mcp.CallToolResult{
104 | 		Content: []interface{}{
105 | 			mcp.TextContent{
106 | 				Type: "text",
107 | 				Text: fmt.Sprintf("Allowed datasets: %s", strings.Join(s.datasets, ", ")),
108 | 			},
109 | 		},
110 | 	}, nil
111 | }
112 | 
113 | func (s *BigQueryServer) handleListTables(
114 | 	ctx context.Context,
115 | 	request mcp.CallToolRequest,
116 | ) (*mcp.CallToolResult, error) {
117 | 	arguments := request.Params.Arguments
118 | 	dataset, ok := arguments["dataset"].(string)
119 | 	if !ok {
120 | 		return nil, fmt.Errorf("dataset must be a string")
121 | 	}
122 | 	if !slices.Contains(s.datasets, dataset) {
123 | 		return nil, fmt.Errorf("dataset %s not allowed", dataset)
124 | 	}
125 | 
126 | 	tables, err := s.client.ListTables(ctx, dataset)
127 | 	if err != nil {
128 | 		return nil, err
129 | 	}
130 | 
131 | 	var tablesStr string
132 | 	for _, table := range tables {
133 | 		tablesStr += fmt.Sprintf("- %s\n", table)
134 | 	}
135 | 
136 | 	return &mcp.CallToolResult{
137 | 		Content: []interface{}{
138 | 			mcp.TextContent{
139 | 				Type: "text",
140 | 				Text: fmt.Sprintf("Tables in dataset `%s`:\n\n%s", dataset, tablesStr),
141 | 			},
142 | 		},
143 | 	}, nil
144 | }
145 | 
146 | func (s *BigQueryServer) handleGetTableSchema(
147 | 	ctx context.Context,
148 | 	request mcp.CallToolRequest,
149 | ) (*mcp.CallToolResult, error) {
150 | 	arguments := request.Params.Arguments
151 | 	dataset, ok := arguments["dataset"].(string)
152 | 	if !ok {
153 | 		return nil, fmt.Errorf("dataset must be a string")
154 | 	}
155 | 	if !slices.Contains(s.datasets, dataset) {
156 | 		return nil, fmt.Errorf("dataset %s not allowed", dataset)
157 | 	}
158 | 	table, ok := arguments["table"].(string)
159 | 	if !ok {
160 | 		return nil, fmt.Errorf("table must be a string")
161 | 	}
162 | 
163 | 	schema, err := s.client.GetTableSchema(ctx, dataset, table)
164 | 	if err != nil {
165 | 		return nil, err
166 | 	}
167 | 
168 | 	var schemaStr string
169 | 	for _, field := range schema {
170 | 		schemaStr += fmt.Sprintf("- %s (%s)\n", field.Name, field.Type)
171 | 		if field.Description != "" {
172 | 			schemaStr += fmt.Sprintf("  Description: %s\n", field.Description)
173 | 		}
174 | 		if field.Repeated {
175 | 			schemaStr += "  Repeated: true\n"
176 | 		}
177 | 		if field.Required {
178 | 			schemaStr += "  Required: true\n"
179 | 		}
180 | 		schemaStr += "\n"
181 | 	}
182 | 
183 | 	return &mcp.CallToolResult{
184 | 		Content: []interface{}{
185 | 			mcp.TextContent{
186 | 				Type: "text",
187 | 				Text: fmt.Sprintf("Schema for table %s in dataset %s:\n\n%s", table, dataset, schemaStr),
188 | 			},
189 | 		},
190 | 	}, nil
191 | }
192 | 
193 | func (s *BigQueryServer) handleDryRunQuery(
194 | 	ctx context.Context,
195 | 	request mcp.CallToolRequest,
196 | ) (*mcp.CallToolResult, error) {
197 | 	arguments := request.Params.Arguments
198 | 	dataset, ok := arguments["dataset"].(string)
199 | 	if !ok {
200 | 		return nil, fmt.Errorf("dataset must be a string")
201 | 	}
202 | 	if !slices.Contains(s.datasets, dataset) {
203 | 		return nil, fmt.Errorf("dataset %s not allowed", dataset)
204 | 	}
205 | 	query, ok := arguments["query"].(string)
206 | 	if !ok {
207 | 		return nil, fmt.Errorf("query must be a string")
208 | 	}
209 | 
210 | 	status, err := s.client.DryRunQuery(ctx, query, dataset)
211 | 	if err != nil {
212 | 		return nil, err
213 | 	}
214 | 	errors := status.Errors
215 | 	totalBytesProcessed := status.Statistics.TotalBytesProcessed
216 | 	return &mcp.CallToolResult{
217 | 		Content: []interface{}{
218 | 			mcp.TextContent{
219 | 				Type: "text",
220 | 				Text: fmt.Sprintf("Errors: %v\nTotal bytes processed: %d", errors, totalBytesProcessed),
221 | 			},
222 | 		},
223 | 	}, nil
224 | }
225 | 
```