# 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]
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 |
```