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