#
tokens: 3838/50000 4/4 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── CLAUDE.md
├── go.mod
├── go.sum
├── main.go
└── Readme.md
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | snowflake-mcp
2 | 
```

--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------

```markdown
 1 | An MCP for querying Snowflake. External browser auth is the only one
 2 | supported in order to avoid storing Snowflake credentials on disk.
 3 | 
 4 | **WARNING: No attempt has been made to disallow writes. Your only
 5 | defence against a malicious/misbehaving LLM is the permissions you grant
 6 | to the Snowflake account.**
 7 | 
 8 | ## Use with Claude Code CLI
 9 | 
10 | ```sh
11 | claude mcp add-json snowflake '{
12 |   "command": "go",
13 |   "args": [
14 |     "run",
15 |     "github.com/oxplot/snowflake-mcp@latest",
16 |     "-account=PPXXXXX-XXXXXXX",
17 |     "-role=reporter"
18 |   ]
19 | }'
20 | 
21 | ```
22 | 
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Snowflake MCP - Claude Guidelines
 2 | 
 3 | ## Commands
 4 | - Build: `go build`
 5 | - Run: `go run main.go -account=<account> -role=<role> -warehouse=<warehouse>` 
 6 | - Test: `go test ./...`
 7 | - Test single package: `go test ./path/to/package`
 8 | - Test with verbose output: `go test -v ./...`
 9 | - Lint: `go vet ./...`
10 | - Format code: `go fmt ./...`
11 | 
12 | ## Code Style Guidelines
13 | - **Imports**: Group imports by standard library, external, then project imports
14 | - **Error Handling**: Return errors with context using `fmt.Errorf("Failed to...: %w", err)`
15 | - **Naming**: Use CamelCase for exported functions/types, camelCase for unexported
16 | - **Types**: Prefer strong types over generic ones like `interface{}`/`any` when possible
17 | - **Functions**: Keep functions focused and under ~100 lines
18 | - **Comments**: Document exported functions and types with meaningful comments
19 | - **Regex**: Compile regexp patterns once and reuse
20 | - **SQL**: Use parameterized queries to prevent SQL injection
21 | 
22 | ## Project Structure
23 | This is a Snowflake MCP (Machine Communication Protocol) client that connects to Snowflake and provides database exploration capabilities via MCP protocol.
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"context"
  6 | 	"database/sql"
  7 | 	"encoding/json"
  8 | 	"flag"
  9 | 	"fmt"
 10 | 	"log"
 11 | 	"regexp"
 12 | 
 13 | 	"github.com/jmoiron/sqlx"
 14 | 
 15 | 	"github.com/mark3labs/mcp-go/mcp"
 16 | 	"github.com/mark3labs/mcp-go/server"
 17 | 	"github.com/snowflakedb/gosnowflake"
 18 | )
 19 | 
 20 | func run() error {
 21 | 	var (
 22 | 		snowflakeAccount   = flag.String("account", "", "Snowflake account name")
 23 | 		snowflakeRole      = flag.String("role", "", "Snowflake role name")
 24 | 		snowflakeWarehouse = flag.String("warehouse", "", "Snowflake warehouse name")
 25 | 	)
 26 | 	flag.Parse()
 27 | 	if *snowflakeAccount == "" || *snowflakeRole == "" {
 28 | 		return fmt.Errorf("Please provide account and role")
 29 | 	}
 30 | 
 31 | 	// Setup connection to snowflake using browser auth
 32 | 
 33 | 	sfconfig := gosnowflake.Config{
 34 | 		Account:       *snowflakeAccount,
 35 | 		Role:          *snowflakeRole,
 36 | 		Warehouse:     *snowflakeWarehouse,
 37 | 		Authenticator: gosnowflake.AuthTypeExternalBrowser,
 38 | 	}
 39 | 	connector := gosnowflake.NewConnector(gosnowflake.SnowflakeDriver{}, sfconfig)
 40 | 	db := sqlx.NewDb(sql.OpenDB(connector), "snowflake").Unsafe()
 41 | 
 42 | 	// Create MCP server
 43 | 
 44 | 	mcpServer := server.NewMCPServer(
 45 | 		"Snowflake",
 46 | 		"1.0.0",
 47 | 		server.WithResourceCapabilities(false, false),
 48 | 	)
 49 | 
 50 | 	mcpServer.AddResource(mcp.NewResource(
 51 | 		"snowflake://",
 52 | 		"Database list",
 53 | 		mcp.WithResourceDescription("List of databases"),
 54 | 		mcp.WithMIMEType("text/plain"),
 55 | 	), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
 56 | 		return getNameList(db, "SHOW TERSE DATABASES", func(name string) mcp.ResourceContents {
 57 | 			return mcp.TextResourceContents{
 58 | 				URI:      fmt.Sprintf("snowflake://%s", name),
 59 | 				MIMEType: "text/plain",
 60 | 				Text:     name,
 61 | 			}
 62 | 		})
 63 | 	})
 64 | 
 65 | 	schemaPat := regexp.MustCompile(`^snowflake://([^/]+)$`)
 66 | 	mcpServer.AddResourceTemplate(mcp.NewResourceTemplate(
 67 | 		"snowflake://{database-name}",
 68 | 		"Schema list in database",
 69 | 		mcp.WithTemplateDescription("List of schemas in a database"),
 70 | 		mcp.WithTemplateMIMEType("text/plain"),
 71 | 	), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
 72 | 		m := schemaPat.FindStringSubmatch(request.Params.URI)
 73 | 		if m == nil {
 74 | 			return nil, fmt.Errorf("Invalid URI")
 75 | 		}
 76 | 		dbName := m[1]
 77 | 		return getNameList(db, fmt.Sprintf(`SHOW TERSE SCHEMAS IN DATABASE %s`, dbName), func(name string) mcp.ResourceContents {
 78 | 			return mcp.TextResourceContents{
 79 | 				URI:      fmt.Sprintf("snowflake://%s/%s", dbName, name),
 80 | 				MIMEType: "text/plain",
 81 | 				Text:     name,
 82 | 			}
 83 | 		})
 84 | 	})
 85 | 
 86 | 	tablesPat := regexp.MustCompile(`^snowflake://([^/]+)/([^/]+)/tables$`)
 87 | 	mcpServer.AddResourceTemplate(mcp.NewResourceTemplate(
 88 | 		"snowflake://{database-name}/{schema-name}/tables",
 89 | 		"Table list in schema",
 90 | 		mcp.WithTemplateDescription("List of tables in a schema"),
 91 | 		mcp.WithTemplateMIMEType("text/plain"),
 92 | 	), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
 93 | 		m := tablesPat.FindStringSubmatch(request.Params.URI)
 94 | 		if m == nil {
 95 | 			return nil, fmt.Errorf("Invalid URI")
 96 | 		}
 97 | 		dbName := m[1]
 98 | 		schemaName := m[2]
 99 | 
100 | 		return getNameList(db, fmt.Sprintf(`SHOW TERSE TABLES IN SCHEMA %s.%s`, dbName, schemaName), func(name string) mcp.ResourceContents {
101 | 			return mcp.TextResourceContents{
102 | 				URI:      fmt.Sprintf("snowflake://%s/%s/table/%s", dbName, schemaName, name),
103 | 				MIMEType: "text/plain",
104 | 				Text:     name,
105 | 			}
106 | 		})
107 | 	})
108 | 
109 | 	viewsPat := regexp.MustCompile(`^snowflake://([^/]+)/([^/]+)/views$`)
110 | 	mcpServer.AddResourceTemplate(mcp.NewResourceTemplate(
111 | 		"snowflake://{database-name}/{schema-name}/views",
112 | 		"View list in schema",
113 | 		mcp.WithTemplateDescription("List of views in a schema"),
114 | 		mcp.WithTemplateMIMEType("text/plain"),
115 | 	), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
116 | 		m := viewsPat.FindStringSubmatch(request.Params.URI)
117 | 		if m == nil {
118 | 			return nil, fmt.Errorf("Invalid URI")
119 | 		}
120 | 		dbName, schemaName := m[1], m[2]
121 | 		return getNameList(db, fmt.Sprintf(`SHOW TERSE TABLES IN SCHEMA %s.%s`, dbName, schemaName), func(name string) mcp.ResourceContents {
122 | 			return mcp.TextResourceContents{
123 | 				URI:      fmt.Sprintf("snowflake://%s/%s/view/%s", dbName, schemaName, name),
124 | 				MIMEType: "text/plain",
125 | 				Text:     name,
126 | 			}
127 | 		})
128 | 	})
129 | 
130 | 	defPat := regexp.MustCompile(`^snowflake://([^/]+)/([^/]+)/(?:view|table)/([^/]+)$`)
131 | 	vtDefHandler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
132 | 		m := defPat.FindStringSubmatch(request.Params.URI)
133 | 		if m == nil {
134 | 			return nil, fmt.Errorf("Invalid URI")
135 | 		}
136 | 		dbName, schemaName, tableName := m[1], m[2], m[3]
137 | 		rows, err := db.Queryx(fmt.Sprintf("DESCRIBE TABLE %s.%s.%s", dbName, schemaName, tableName))
138 | 		if err != nil {
139 | 			return nil, fmt.Errorf("Failed to get table def for %s.%s.%s: %v", dbName, schemaName, tableName, err)
140 | 		}
141 | 		defer rows.Close()
142 | 
143 | 		type column struct {
144 | 			Name string `db:"name" json:"name"`
145 | 			Type string `db:"type" json:"type"`
146 | 			Kind string `db:"kind" json:"-"`
147 | 		}
148 | 
149 | 		columns := []column{}
150 | 		for rows.Next() {
151 | 			t := column{}
152 | 			if err = rows.StructScan(&t); err != nil {
153 | 				return nil, fmt.Errorf("Failed to scan rows: %v", err)
154 | 			}
155 | 			if t.Kind != "COLUMN" {
156 | 				continue
157 | 			}
158 | 			columns = append(columns, column(t))
159 | 		}
160 | 
161 | 		b := bytes.NewBuffer(nil)
162 | 		enc := json.NewEncoder(b)
163 | 		enc.SetIndent("", " ")
164 | 		if err := enc.Encode(map[string]any{
165 | 			"columns": columns,
166 | 		}); err != nil {
167 | 			return nil, fmt.Errorf("Failed to marshal result: %v", err)
168 | 		}
169 | 
170 | 		return []mcp.ResourceContents{
171 | 			mcp.TextResourceContents{
172 | 				URI:      request.Params.URI,
173 | 				MIMEType: "application/json",
174 | 				Text:     b.String(),
175 | 			},
176 | 		}, nil
177 | 	}
178 | 
179 | 	mcpServer.AddResourceTemplate(mcp.NewResourceTemplate(
180 | 		"snowflake://{database-name}/{schema-name}/table/{table-name}",
181 | 		"Table definition",
182 | 		mcp.WithTemplateDescription("Definition of a table including columns and column types"),
183 | 		mcp.WithTemplateMIMEType("application/json"),
184 | 	), vtDefHandler)
185 | 
186 | 	mcpServer.AddResourceTemplate(mcp.NewResourceTemplate(
187 | 		"snowflake://{database-name}/{schema-name}/view/{table-name}",
188 | 		"View definition",
189 | 		mcp.WithTemplateDescription("Definition of a view including columns and column types"),
190 | 		mcp.WithTemplateMIMEType("application/json"),
191 | 	), vtDefHandler)
192 | 
193 | 	// Add a query tool.
194 | 	mcpServer.AddTool(mcp.NewTool(
195 | 		"query",
196 | 		mcp.WithDescription("Execute a SQL query."),
197 | 		mcp.WithString("query",
198 | 			mcp.Required(),
199 | 			mcp.Description("SQL query to execute.  You must use full database.schema.table when referencing tables."),
200 | 		),
201 | 	), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
202 | 
203 | 		query, _ := request.Params.Arguments["query"].(string)
204 | 		const maxResultRows = 1000
205 | 
206 | 		// Execute the query.
207 | 		rows, err := db.QueryxContext(ctx, query)
208 | 		if err != nil {
209 | 			return nil, fmt.Errorf("Failed to execute query: %v", err)
210 | 		}
211 | 		defer rows.Close()
212 | 
213 | 		// Get column details.
214 | 		columnInfo := []map[string]any{}
215 | 		columnTypes, err := rows.ColumnTypes()
216 | 		if err != nil {
217 | 			return nil, fmt.Errorf("Failed to get column types: %v", err)
218 | 		}
219 | 		for _, columnType := range columnTypes {
220 | 			columnInfo = append(columnInfo, map[string]any{
221 | 				"name": columnType.Name(),
222 | 				"type": columnType.DatabaseTypeName(),
223 | 			})
224 | 		}
225 | 
226 | 		// Fetch the rows.
227 | 		rowsSlice := [][]any{}
228 | 		for rows.Next() {
229 | 			r := []any{}
230 | 			r, err := rows.SliceScan()
231 | 			if err != nil {
232 | 				return nil, fmt.Errorf("Failed to scan row: %v", err)
233 | 			}
234 | 			rowsSlice = append(rowsSlice, r)
235 | 			if len(rowsSlice) >= maxResultRows {
236 | 				break
237 | 			}
238 | 		}
239 | 
240 | 		result := map[string]any{
241 | 			"column_info": columnInfo,
242 | 			"rows":        rowsSlice,
243 | 			"notice":      fmt.Sprintf("Only first %d rows are shown", maxResultRows),
244 | 		}
245 | 		b := bytes.NewBuffer(nil)
246 | 		jsonEnc := json.NewEncoder(b)
247 | 		jsonEnc.SetIndent("", " ")
248 | 		if err := jsonEnc.Encode(result); err != nil {
249 | 			return nil, fmt.Errorf("Failed to marshal result: %v", err)
250 | 		}
251 | 
252 | 		return &mcp.CallToolResult{
253 | 			Content: []mcp.Content{
254 | 				mcp.TextContent{
255 | 					Type: "text",
256 | 					Text: b.String(),
257 | 				},
258 | 			},
259 | 		}, nil
260 | 	})
261 | 	return server.ServeStdio(mcpServer)
262 | }
263 | 
264 | func getNameList[T any](db *sqlx.DB, query string, conv func(name string) T) ([]T, error) {
265 | 	rows, err := db.Queryx(query)
266 | 	if err != nil {
267 | 		return nil, fmt.Errorf("Failed to run query '%s': %v", query, err)
268 | 	}
269 | 	defer rows.Close()
270 | 
271 | 	ret := []T{}
272 | 	for rows.Next() {
273 | 		t := struct {
274 | 			Name string `db:"name"`
275 | 		}{}
276 | 		if err = rows.StructScan(&t); err != nil {
277 | 			return nil, fmt.Errorf("Failed to scan rows: %v", err)
278 | 		}
279 | 		ret = append(ret, conv(t.Name))
280 | 	}
281 | 	return ret, nil
282 | }
283 | 
284 | func main() {
285 | 	if err := run(); err != nil {
286 | 		log.Fatalf("Error: %v", err)
287 | 	}
288 | }
289 | 
```