Skip to content

Commit daf513b

Browse files
Ajit Pratap Singhclaude
authored andcommitted
feat: add MCP server exposing GoSQLX SQL tools over streamable HTTP
Adds a Model Context Protocol server that wraps GoSQLX's SQL processing capabilities as MCP tools accessible to AI assistants and MCP clients. ## New packages - `pkg/mcp/` — importable library (Config, Server, 7 tool handlers) - `cmd/gosqlx-mcp/` — standalone binary with graceful shutdown ## Tools - validate_sql — syntax validation with optional dialect (7 dialects) - format_sql — formatting with indent/case/semicolon options - parse_sql — AST summary (statement count + types) - extract_metadata — extract tables, columns, functions - security_scan — SQL injection pattern detection (8 pattern types) - lint_sql — style rules L001–L010 - analyze_sql — concurrent fan-out across all 6 tools ## Transport & auth - Streamable HTTP transport (mark3labs/mcp-go v0.45.0) - Optional bearer token auth via GOSQLX_MCP_AUTH_TOKEN env var ## Tasks added - task mcp — run server - task mcp:build — build binary - task mcp:test — race-detected tests - task mcp:install — install globally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e9cf54b commit daf513b

14 files changed

Lines changed: 1666 additions & 1 deletion

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# Built binary executables (only in root directory)
1212
/gosqlx
13+
/gosqlx-mcp
1314

1415
# Output of the go coverage tool, specifically when used with LiteIDE
1516
*.out

Taskfile.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,33 @@ tasks:
298298
cmds:
299299
- go run ./cmd/gosqlx lsp --log /tmp/gosqlx-lsp.log
300300

301+
# =============================================================================
302+
# MCP SERVER
303+
# =============================================================================
304+
mcp:
305+
desc: Run the MCP server (env vars GOSQLX_MCP_HOST, GOSQLX_MCP_PORT, GOSQLX_MCP_AUTH_TOKEN)
306+
cmds:
307+
- go run ./cmd/gosqlx-mcp
308+
309+
mcp:build:
310+
desc: Build the MCP server binary to build/gosqlx-mcp
311+
cmds:
312+
- echo "Building MCP server binary..."
313+
- go build -v -o {{.BUILD_DIR}}/gosqlx-mcp ./cmd/gosqlx-mcp
314+
generates:
315+
- '{{.BUILD_DIR}}/gosqlx-mcp'
316+
317+
mcp:test:
318+
desc: Run MCP package tests with race detection
319+
cmds:
320+
- echo "Testing MCP package..."
321+
- go test -race -timeout 60s ./pkg/mcp/... ./cmd/gosqlx-mcp/...
322+
323+
mcp:install:
324+
desc: Install gosqlx-mcp binary globally
325+
cmds:
326+
- go install ./cmd/gosqlx-mcp
327+
301328
# =============================================================================
302329
# EXAMPLES
303330
# =============================================================================

cmd/gosqlx-mcp/main.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package main is the entry point for the gosqlx-mcp MCP server.
16+
//
17+
// The server exposes GoSQLX SQL processing capabilities as MCP tools
18+
// accessible over streamable HTTP transport.
19+
//
20+
// # Environment variables
21+
//
22+
// GOSQLX_MCP_HOST bind host (default: 127.0.0.1)
23+
// GOSQLX_MCP_PORT bind port (default: 8080)
24+
// GOSQLX_MCP_AUTH_TOKEN bearer token; empty disables auth
25+
//
26+
// # Usage
27+
//
28+
// gosqlx-mcp
29+
// GOSQLX_MCP_PORT=9090 gosqlx-mcp
30+
// GOSQLX_MCP_AUTH_TOKEN=secret gosqlx-mcp
31+
package main
32+
33+
import (
34+
"context"
35+
"fmt"
36+
"os"
37+
"os/signal"
38+
"syscall"
39+
40+
gosqlxmcp "github.com/ajitpratap0/GoSQLX/pkg/mcp"
41+
)
42+
43+
func main() {
44+
cfg, err := gosqlxmcp.LoadConfig()
45+
if err != nil {
46+
fmt.Fprintf(os.Stderr, "gosqlx-mcp: configuration error: %v\n", err)
47+
os.Exit(1)
48+
}
49+
50+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
51+
defer stop()
52+
53+
srv := gosqlxmcp.New(cfg)
54+
if err := srv.Start(ctx); err != nil {
55+
fmt.Fprintf(os.Stderr, "gosqlx-mcp: %v\n", err)
56+
os.Exit(1)
57+
}
58+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# MCP Server Design: GoSQLX
2+
3+
**Date:** 2026-03-08
4+
**Status:** Implemented
5+
6+
## Overview
7+
8+
GoSQLX MCP server exposes SQL processing capabilities as MCP tools over streamable HTTP transport, enabling AI assistants and MCP-compatible clients to parse, validate, format, lint, and security-scan SQL.
9+
10+
## Design Decisions
11+
12+
| Decision | Choice | Rationale |
13+
|---|---|---|
14+
| Transport | Streamable HTTP only | Required for non-subprocess clients |
15+
| MCP library | `mark3labs/mcp-go` | Mature HTTP transport; simpler API vs official SDK |
16+
| Auth | Optional bearer token (env var) | Secure by default when configured, zero-config for local use |
17+
| Structure | `pkg/mcp/` + `cmd/gosqlx-mcp/` | Library importable; binary for direct use |
18+
19+
## Tools
20+
21+
| Tool | Wraps | Purpose |
22+
|---|---|---|
23+
| `validate_sql` | `gosqlx.Validate` / `ParseWithDialect` | Syntax validation with dialect support |
24+
| `format_sql` | `gosqlx.Format` | Formatting with configurable options |
25+
| `parse_sql` | `gosqlx.Parse` | AST summary (statement count and types) |
26+
| `extract_metadata` | `gosqlx.ExtractMetadata` | Tables, columns, functions |
27+
| `security_scan` | `security.Scanner.ScanSQL` | SQL injection pattern detection |
28+
| `lint_sql` | `linter.Linter.LintString` | Style rules L001–L010 |
29+
| `analyze_sql` | All 6 above (concurrent) | Composite one-call analysis |
30+
31+
## Configuration
32+
33+
| Env Var | Default | Description |
34+
|---|---|---|
35+
| `GOSQLX_MCP_HOST` | `127.0.0.1` | Bind interface |
36+
| `GOSQLX_MCP_PORT` | `8080` | TCP port (1–65535) |
37+
| `GOSQLX_MCP_AUTH_TOKEN` | `""` | Bearer token; empty disables auth |
38+
39+
## Architecture
40+
41+
```
42+
cmd/gosqlx-mcp/main.go → LoadConfig → New → Start
43+
pkg/mcp/
44+
config.go → Config, LoadConfig, DefaultConfig
45+
middleware.go → BearerAuthMiddleware
46+
tools.go → 7 handlers + internal result functions
47+
server.go → Server, New, Start, registerTools
48+
```
49+
50+
## Key Patterns
51+
52+
- **Internal result functions**: Each tool has a `*Internal()` function returning `map[string]any` called by both the MCP handler and `analyze_sql`'s concurrent fan-out
53+
- **Fan-out**: `analyze_sql` uses `sync.WaitGroup` + buffered channel to run all 6 tools concurrently
54+
- **Thin wrappers**: Zero new business logic — all SQL processing delegates to existing GoSQLX packages
55+
- **Consistent imports**: Rule constructors mirror `cmd/gosqlx/cmd/lint.go:createLinter()` exactly
56+
57+
## Quick Start
58+
59+
```bash
60+
# Run server (default: localhost:8080, no auth)
61+
gosqlx-mcp
62+
63+
# With auth
64+
GOSQLX_MCP_AUTH_TOKEN=secret gosqlx-mcp
65+
66+
# Build binary
67+
task mcp:build
68+
69+
# Run tests
70+
task mcp:test
71+
```

go.mod

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
module github.com/ajitpratap0/GoSQLX
22

3-
go 1.21
3+
go 1.23.0
44

55
require (
66
github.com/fsnotify/fsnotify v1.9.0
7+
github.com/mark3labs/mcp-go v0.45.0
78
github.com/spf13/cobra v1.10.1
89
github.com/spf13/pflag v1.0.9
910
golang.org/x/term v0.20.0
1011
gopkg.in/yaml.v3 v3.0.1
1112
)
1213

1314
require (
15+
github.com/bahlo/generic-list-go v0.2.0 // indirect
16+
github.com/buger/jsonparser v1.1.1 // indirect
17+
github.com/google/uuid v1.6.0 // indirect
1418
github.com/inconshreveable/mousetrap v1.1.0 // indirect
19+
github.com/invopop/jsonschema v0.13.0 // indirect
20+
github.com/mailru/easyjson v0.7.7 // indirect
21+
github.com/spf13/cast v1.7.1 // indirect
22+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
23+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
1524
golang.org/x/sys v0.20.0 // indirect
1625
)

go.sum

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
1+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
15
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
6+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
9+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
210
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
311
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
12+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
13+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
14+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
15+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
416
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
517
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
18+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
19+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
20+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
21+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
22+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
23+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
26+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
27+
github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
28+
github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
29+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
32+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
633
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
34+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
35+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
736
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
837
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
938
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
1039
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
40+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
41+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
42+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
43+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
44+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
45+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
1146
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
1247
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1348
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=

pkg/mcp/config.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package mcp provides a Model Context Protocol (MCP) server for GoSQLX.
16+
// It exposes SQL parsing, validation, formatting, linting, and security
17+
// scanning as MCP tools accessible over streamable HTTP transport.
18+
//
19+
// # Quick start
20+
//
21+
// cfg, err := mcp.LoadConfig()
22+
// if err != nil {
23+
// log.Fatal(err)
24+
// }
25+
// srv := mcp.New(cfg)
26+
// srv.Start(context.Background())
27+
//
28+
// # Environment variables
29+
//
30+
// GOSQLX_MCP_HOST bind host (default: 127.0.0.1)
31+
// GOSQLX_MCP_PORT bind port (default: 8080)
32+
// GOSQLX_MCP_AUTH_TOKEN bearer token; empty disables auth
33+
package mcp
34+
35+
import (
36+
"fmt"
37+
"os"
38+
"strconv"
39+
"strings"
40+
)
41+
42+
// Config holds all MCP server configuration loaded from environment variables.
43+
// Use LoadConfig or DefaultConfig to obtain a valid Config; the zero value is not valid.
44+
type Config struct {
45+
// Host is the interface to bind to.
46+
// Source: GOSQLX_MCP_HOST (default: "127.0.0.1")
47+
Host string
48+
49+
// Port is the TCP port to listen on (1–65535).
50+
// Source: GOSQLX_MCP_PORT (default: 8080)
51+
Port int
52+
53+
// AuthToken is the optional bearer token for request authentication.
54+
// When non-empty every request must carry "Authorization: Bearer <token>".
55+
// Source: GOSQLX_MCP_AUTH_TOKEN (default: "" — auth disabled)
56+
AuthToken string
57+
}
58+
59+
// LoadConfig reads configuration from environment variables, applying defaults
60+
// for any variables that are unset or empty.
61+
func LoadConfig() (*Config, error) {
62+
cfg := DefaultConfig()
63+
64+
if v := os.Getenv("GOSQLX_MCP_HOST"); v != "" {
65+
cfg.Host = v
66+
}
67+
68+
if v := os.Getenv("GOSQLX_MCP_PORT"); v != "" {
69+
port, err := strconv.Atoi(v)
70+
if err != nil {
71+
return nil, fmt.Errorf("GOSQLX_MCP_PORT: expected integer, got %q", v)
72+
}
73+
if port < 1 || port > 65535 {
74+
return nil, fmt.Errorf("GOSQLX_MCP_PORT: %d is out of range (1–65535)", port)
75+
}
76+
cfg.Port = port
77+
}
78+
79+
if v := strings.TrimSpace(os.Getenv("GOSQLX_MCP_AUTH_TOKEN")); v != "" {
80+
cfg.AuthToken = v
81+
}
82+
83+
return cfg, nil
84+
}
85+
86+
// DefaultConfig returns a Config with all defaults applied (auth disabled).
87+
func DefaultConfig() *Config {
88+
return &Config{
89+
Host: "127.0.0.1",
90+
Port: 8080,
91+
}
92+
}
93+
94+
// Addr returns the "host:port" string suitable for net/http ListenAndServe.
95+
func (c *Config) Addr() string {
96+
return fmt.Sprintf("%s:%d", c.Host, c.Port)
97+
}
98+
99+
// AuthEnabled reports whether bearer token authentication is configured.
100+
func (c *Config) AuthEnabled() bool {
101+
return c.AuthToken != ""
102+
}

0 commit comments

Comments
 (0)