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

```
├── .gitignore
├── cmd
│   └── postgres-mcp
│       └── main.go
├── db.go
├── go.mod
├── go.sum
├── query.go
├── README.md
├── resource_mgr.go
├── server.go
└── Taskfile.yml
```

# Files

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

```
 1 | # If you prefer the allow list template instead of the deny list, see community template:
 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
 3 | #
 4 | # Binaries for programs and plugins
 5 | *.exe
 6 | *.exe~
 7 | *.dll
 8 | *.so
 9 | *.dylib
10 | 
11 | # Test binary, built with `go test -c`
12 | *.test
13 | 
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 | 
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 | 
20 | # Go workspace file
21 | go.work
22 | go.work.sum
23 | 
24 | # env file
25 | .env
26 | .vscode/
27 | 
```

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

```markdown
 1 | # Postgres MCP (Research Project)
 2 | 
 3 | A Model Control Protocol (MCP) server implementation for PostgreSQL databases. This project provides a simple HTTP interface to interact with PostgreSQL databases through MCP, allowing you to execute read-only queries and explore database resources.
 4 | 
 5 | ## Features
 6 | 
 7 | - MCP-compliant HTTP server
 8 | - Read-only SQL query execution
 9 | - Database resource listing
10 | - Environment variable support through `.env` files
11 | - Built with Go and Gin web framework
12 | 
13 | ## Prerequisites
14 | 
15 | - Go 1.23.6 or later
16 | - PostgreSQL database
17 | - Git
18 | 
19 | ## Installation
20 | 
21 | ```bash
22 | git clone https://github.com/ipfans/postgres-mcp.git
23 | cd postgres-mcp
24 | go mod download
25 | ```
26 | 
27 | ## Configuration
28 | 
29 | You can configure the database connection in two ways:
30 | 
31 | 1. Using command-line flags:
32 | 
33 | ```bash
34 | go run cmd/postgres-mcp/main.go -db "postgres://user:password@localhost:5432/dbname"
35 | ```
36 | 
37 | 2. Using environment variables:
38 |    - Create a `.env` file in the project root
39 |    - Add your database URL:
40 |      ```
41 |      DATABASE_URL=postgres://user:password@localhost:5432/dbname
42 |      ```
43 | 
44 | ## Usage
45 | 
46 | 1. Start the server:
47 | 
48 | ```bash
49 | go run cmd/postgres-mcp/main.go
50 | ```
51 | 
52 | The server will start on port 8080 by default.
53 | 
54 | 2. Interact with the MCP endpoints:
55 | 
56 | - List database resources:
57 | 
58 |   ```
59 |   POST http://localhost:8080/mcp
60 |   Content-Type: application/json
61 | 
62 |   {
63 |     "type": "function",
64 |     "name": "resources"
65 |   }
66 |   ```
67 | 
68 | - Execute a read-only query:
69 | 
70 |   ```
71 |   POST http://localhost:8080/mcp
72 |   Content-Type: application/json
73 | 
74 |   {
75 |     "type": "function",
76 |     "name": "query",
77 |     "arguments": {
78 |       "query": "SELECT * FROM your_table LIMIT 10"
79 |     }
80 |   }
81 |   ```
82 | 
83 | ## License
84 | 
85 | This project is open source and available under the MIT License.
86 | 
87 | ## Contributing
88 | 
89 | Contributions are welcome! Please feel free to submit a Pull Request.
90 | 
```

--------------------------------------------------------------------------------
/Taskfile.yml:
--------------------------------------------------------------------------------

```yaml
1 | # https://taskfile.dev
2 | 
3 | version: "3"
4 | 
5 | tasks:
6 |   default:
7 |     cmds:
8 |       - go run cmd/postgres-mcp/main.go -db "postgres://postgres:postgres@localhost:5432/postgres"
9 | 
```

--------------------------------------------------------------------------------
/db.go:
--------------------------------------------------------------------------------

```go
 1 | package postgresmcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 
 6 | 	"github.com/jackc/pgx/v5"
 7 | )
 8 | 
 9 | // DatabaseQuerier 定义数据库查询接口
10 | type DatabaseQuerier interface {
11 | 	Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
12 | 	Begin(ctx context.Context) (pgx.Tx, error)
13 | }
14 | 
```

--------------------------------------------------------------------------------
/cmd/postgres-mcp/main.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"flag"
 5 | 	"fmt"
 6 | 	"os"
 7 | 
 8 | 	postgresmcp "github.com/ipfans/postgres-mcp"
 9 | 	"github.com/joho/godotenv"
10 | )
11 | 
12 | func main() {
13 | 	var dbString string
14 | 	flag.StringVar(&dbString, "db", "", "Database URL. (e.g. postgres://postgres:postgres@localhost:5432/postgres )")
15 | 	flag.Parse()
16 | 
17 | 	if dbString == "" {
18 | 		godotenv.Load()
19 | 		dbString = os.Getenv("DATABASE_URL")
20 | 		if dbString == "" {
21 | 			fmt.Print("Please provide a database URL, DATABASE_URL environment variable or a dotenv file.\n\n")
22 | 			flag.PrintDefaults()
23 | 			return
24 | 		}
25 | 	}
26 | 
27 | 	postgresmcp.Server(dbString)
28 | }
29 | 
```

--------------------------------------------------------------------------------
/resource_mgr.go:
--------------------------------------------------------------------------------

```go
 1 | package postgresmcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 
 7 | 	mcp_golang "github.com/metoro-io/mcp-golang"
 8 | )
 9 | 
10 | // ResourceManager 处理资源相关操作
11 | type ResourceManager struct {
12 | 	db      DatabaseQuerier
13 | 	baseURL string
14 | }
15 | 
16 | func NewResourceManager(db DatabaseQuerier, baseURL string) *ResourceManager {
17 | 	return &ResourceManager{
18 | 		db:      db,
19 | 		baseURL: baseURL,
20 | 	}
21 | }
22 | 
23 | func (rm *ResourceManager) ListResources(ctx context.Context) ([]*mcp_golang.Content, error) {
24 | 	rows, err := rm.db.Query(ctx, `
25 | 		SELECT 
26 | 			t.table_name,
27 | 			array_agg(
28 | 				c.column_name || ' ' || c.data_type
29 | 				ORDER BY c.ordinal_position
30 | 			) as columns
31 | 		FROM information_schema.tables t
32 | 		JOIN information_schema.columns c 
33 | 			ON c.table_name = t.table_name 
34 | 			AND c.table_schema = t.table_schema
35 | 		WHERE t.table_schema = 'public'
36 | 		GROUP BY t.table_name`)
37 | 	if err != nil {
38 | 		return nil, err
39 | 	}
40 | 	defer rows.Close()
41 | 
42 | 	var resources []*mcp_golang.Content
43 | 	for rows.Next() {
44 | 		var tableName string
45 | 		var columns []string
46 | 		if err := rows.Scan(&tableName, &columns); err != nil {
47 | 			return nil, err
48 | 		}
49 | 
50 | 		schemaInfo := fmt.Sprintf("表名: %s\n字段:\n", tableName)
51 | 		for _, col := range columns {
52 | 			schemaInfo += fmt.Sprintf("- %s\n", col)
53 | 		}
54 | 		content := mcp_golang.NewTextContent(schemaInfo)
55 | 		resources = append(resources, content)
56 | 	}
57 | 	return resources, rows.Err()
58 | }
59 | 
```

--------------------------------------------------------------------------------
/query.go:
--------------------------------------------------------------------------------

```go
 1 | package postgresmcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"encoding/json"
 6 | 	"fmt"
 7 | 	"log"
 8 | 
 9 | 	"github.com/jackc/pgx/v5"
10 | 	mcp_golang "github.com/metoro-io/mcp-golang"
11 | )
12 | 
13 | // QueryExecutor 处理数据库查询操作
14 | type QueryExecutor struct {
15 | 	db DatabaseQuerier
16 | }
17 | 
18 | func NewQueryExecutor(db DatabaseQuerier) *QueryExecutor {
19 | 	return &QueryExecutor{db: db}
20 | }
21 | 
22 | func (qe *QueryExecutor) ExecuteReadOnlyQuery(ctx context.Context, query string) (*mcp_golang.ToolResponse, error) {
23 | 	tx, err := qe.db.Begin(ctx)
24 | 	if err != nil {
25 | 		return nil, err
26 | 	}
27 | 	defer func() {
28 | 		if err := tx.Rollback(ctx); err != nil && err != pgx.ErrTxClosed {
29 | 			log.Printf("Could not rollback transaction: %v", err)
30 | 		}
31 | 	}()
32 | 
33 | 	if _, err := tx.Exec(ctx, "BEGIN TRANSACTION READ ONLY"); err != nil {
34 | 		return nil, err
35 | 	}
36 | 
37 | 	rows, err := tx.Query(ctx, query)
38 | 	if err != nil {
39 | 		return nil, err
40 | 	}
41 | 	defer rows.Close()
42 | 
43 | 	results := []map[string]interface{}{}
44 | 	columnNames := rows.FieldDescriptions()
45 | 	for rows.Next() {
46 | 		values, err := rows.Values()
47 | 		if err != nil {
48 | 			return nil, err
49 | 		}
50 | 		rowMap := make(map[string]interface{})
51 | 		for i, col := range columnNames {
52 | 			rowMap[col.Name] = values[i]
53 | 		}
54 | 		results = append(results, rowMap)
55 | 	}
56 | 
57 | 	if err := rows.Err(); err != nil {
58 | 		return nil, err
59 | 	}
60 | 
61 | 	jsonResult, err := json.MarshalIndent(results, "", "  ")
62 | 	if err != nil {
63 | 		return nil, fmt.Errorf("failed to marshal query results to JSON: %w", err)
64 | 	}
65 | 
66 | 	if err := tx.Commit(ctx); err != nil {
67 | 		return nil, err
68 | 	}
69 | 
70 | 	content := mcp_golang.NewTextContent(string(jsonResult))
71 | 	return &mcp_golang.ToolResponse{
72 | 		Content: []*mcp_golang.Content{content},
73 | 	}, nil
74 | }
75 | 
```

--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------

```go
 1 | package postgresmcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"log"
 7 | 
 8 | 	"github.com/gin-gonic/gin"
 9 | 	"github.com/jackc/pgx/v5/pgxpool"
10 | 	mcp_golang "github.com/metoro-io/mcp-golang"
11 | 	"github.com/metoro-io/mcp-golang/transport/http"
12 | )
13 | 
14 | // MCPServer 处理MCP服务器配置和启动
15 | type MCPServer struct {
16 | 	server    *mcp_golang.Server
17 | 	transport *http.GinTransport
18 | 	resources *ResourceManager
19 | 	queryExec *QueryExecutor
20 | }
21 | 
22 | type QueryArguments struct {
23 | 	Query string `json:"query" jsonschema:"required,description=The sql query to execute"`
24 | }
25 | 
26 | func NewMCPServer(dbString string, baseURL string) (*MCPServer, error) {
27 | 	dbpool, err := pgxpool.New(context.Background(), dbString)
28 | 	if err != nil {
29 | 		return nil, fmt.Errorf("unable to connect to database: %w", err)
30 | 	}
31 | 
32 | 	transport := http.NewGinTransport()
33 | 	server := mcp_golang.NewServer(transport)
34 | 	resources := NewResourceManager(dbpool, baseURL)
35 | 	queryExec := NewQueryExecutor(dbpool)
36 | 
37 | 	return &MCPServer{
38 | 		server:    server,
39 | 		transport: transport,
40 | 		resources: resources,
41 | 		queryExec: queryExec,
42 | 	}, nil
43 | }
44 | 
45 | func (s *MCPServer) registerHandlers() error {
46 | 	// 注册资源处理器
47 | 	if err := s.server.RegisterResource(
48 | 		"resources",
49 | 		"Database Resources",
50 | 		"List all database tables",
51 | 		"application/json",
52 | 		s.resources.ListResources,
53 | 	); err != nil {
54 | 		return err
55 | 	}
56 | 
57 | 	// 注册查询工具
58 | 	if err := s.server.RegisterTool(
59 | 		"query",
60 | 		"Run a read-only SQL query",
61 | 		func(ctx context.Context, req QueryArguments) (*mcp_golang.ToolResponse, error) {
62 | 			return s.queryExec.ExecuteReadOnlyQuery(ctx, req.Query)
63 | 		},
64 | 	); err != nil {
65 | 		return err
66 | 	}
67 | 
68 | 	return nil
69 | }
70 | 
71 | func (s *MCPServer) Start() error {
72 | 	if err := s.registerHandlers(); err != nil {
73 | 		return err
74 | 	}
75 | 
76 | 	router := gin.Default()
77 | 	router.POST("/mcp", s.transport.Handler())
78 | 	return router.Run(":8080")
79 | }
80 | 
81 | // Server 是主入口函数
82 | func Server(dbString string) {
83 | 	baseURL := "http://localhost:8080/resources"
84 | 	server, err := NewMCPServer(dbString, baseURL)
85 | 	if err != nil {
86 | 		log.Fatalf("Failed to create server: %v", err)
87 | 		return
88 | 	}
89 | 
90 | 	if err := server.Start(); err != nil {
91 | 		log.Fatalf("Failed to start server: %v", err)
92 | 	}
93 | }
94 | 
```