Skip to content

Commit c12e674

Browse files
authored
feat: add mcp server support (#42)
* feat: add mcp server support * upgrade go from 1.22 to 1.23 --------- Co-authored-by: Rick <[email protected]>
1 parent 83fe326 commit c12e674

File tree

11 files changed

+239
-18
lines changed

11 files changed

+239
-18
lines changed

.github/workflows/build.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
- name: Set up Go
1111
uses: actions/setup-go@v3
1212
with:
13-
go-version: 1.22.x
13+
go-version: 1.23.x
1414
- uses: actions/[email protected]
1515
- name: Unit Test
1616
run: |
@@ -29,7 +29,7 @@ jobs:
2929
- name: Set up Go
3030
uses: actions/setup-go@v3
3131
with:
32-
go-version: 1.22.x
32+
go-version: 1.23.x
3333
- uses: actions/[email protected]
3434
- name: Run GoReleaser
3535
uses: goreleaser/goreleaser-action@v6

.github/workflows/release.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Set up Go
2121
uses: actions/setup-go@v3
2222
with:
23-
go-version: 1.22.x
23+
go-version: 1.23.x
2424
- uses: actions/[email protected]
2525
- name: Unit Test
2626
run: |
@@ -48,7 +48,7 @@ jobs:
4848
- name: Set up Go
4949
uses: actions/setup-go@v3
5050
with:
51-
go-version: 1.22.x
51+
go-version: 1.23.x
5252
- name: Image Registry Login
5353
run: |
5454
docker login --username linuxsuren --password ${{secrets.DOCKER_HUB_PUBLISH_SECRETS}}

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG GO_BUILDER=docker.io/library/golang:1.22
1+
ARG GO_BUILDER=docker.io/library/golang:1.23
22
ARG BASE_IMAGE=docker.io/library/alpine:3.12
33

44
FROM ${GO_BUILDER} AS builder

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,34 @@ This project provides an ORM-based database store extension for API testing, sim
99
- Simplified database operations using ORM.
1010
- Integration with API testing frameworks.
1111
- Support for multiple databases (SQLite, MySQL, PostgreSQL, TDengine, etc.).
12+
- Database query as a MCP server
1213

1314
## Usage
1415
To use this extension in your API testing project, follow these steps:
1516
1. Install the necessary dependencies.
1617
2. Configure the database connection settings.
1718
3. Integrate the extension into your API tests.
1819

20+
## MCP Server
21+
22+
```json
23+
{
24+
"mcpServers": {
25+
"database-mcp": {
26+
"name": "database-mcp",
27+
"type": "stdio",
28+
"description": "Database query MCP Server",
29+
"isActive": true,
30+
"command": "atest-store-orm",
31+
"args": [
32+
"mcp",
33+
"--mode=stdio"
34+
]
35+
}
36+
}
37+
}
38+
```
39+
1940
## Quick MySQL Setup with TiUP Playground
2041

2142
You can quickly set up a MySQL-compatible database using [TiUP Playground](https://docs.pingcap.com/tidb/stable/tiup-playground):

cmd/mcp.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
Copyright 2025 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
"net/http"
21+
"os"
22+
23+
"github.com/linuxsuren/api-testing/pkg/testing"
24+
"github.com/linuxsuren/atest-ext-store-orm/pkg"
25+
"github.com/modelcontextprotocol/go-sdk/mcp"
26+
"github.com/spf13/cobra"
27+
)
28+
29+
func newMCPCommand() (c *cobra.Command) {
30+
opt := &mcpOption{}
31+
c = &cobra.Command{
32+
Use: "mcp",
33+
Short: "Multi-Cluster-Platform related commands",
34+
PreRunE: opt.preRunE,
35+
RunE: opt.runE,
36+
}
37+
flags := c.Flags()
38+
flags.StringVarP(&opt.mode, "mode", "", "http", "Server mode, one of http/stdio/sse")
39+
flags.IntVarP(&opt.port, "port", "", 7072, "Server port for http or sse mode")
40+
flags.StringVarP(&opt.url, "url", "", "", "Database URL")
41+
flags.StringVarP(&opt.username, "username", "", "", "Database username")
42+
flags.StringVarP(&opt.password, "password", "", "", "Database password")
43+
flags.StringVarP(&opt.database, "database", "", "", "Database name")
44+
flags.StringVarP(&opt.driver, "driver", "", "mysql", "Database driver, one of mysql/postgres/sqlite")
45+
return
46+
}
47+
48+
type mcpOption struct {
49+
mode string
50+
port int
51+
url string
52+
username string
53+
password string
54+
database string
55+
driver string
56+
}
57+
58+
func (o *mcpOption) preRunE(c *cobra.Command, args []string) (err error) {
59+
if o.url = getValueOrEnv(o.url, "DB_URL"); o.url == "" {
60+
err = fmt.Errorf("database url is required")
61+
return
62+
}
63+
o.username = getValueOrEnv(o.username, "DB_USERNAME")
64+
o.password = getValueOrEnv(o.password, "DB_PASSWORD")
65+
o.database = getValueOrEnv(o.database, "DB_DATABASE")
66+
o.driver = getValueOrEnv(o.driver, "DB_DRIVER")
67+
return
68+
}
69+
70+
func getValueOrEnv(value, envKey string) (result string) {
71+
if value != "" {
72+
result = value
73+
} else {
74+
result = os.Getenv(envKey)
75+
}
76+
return
77+
}
78+
79+
func (o *mcpOption) runE(c *cobra.Command, args []string) (err error) {
80+
opts := &mcp.ServerOptions{
81+
Instructions: "Database query mcp server",
82+
}
83+
84+
server := mcp.NewServer(&mcp.Implementation{
85+
Name: "database-mcp-server",
86+
Title: "ORM Database MCP Server",
87+
}, opts)
88+
89+
store := &testing.Store{
90+
URL: o.url,
91+
Username: o.username,
92+
Password: o.password,
93+
Properties: map[string]string{
94+
"database": o.database,
95+
"driver": o.driver,
96+
},
97+
}
98+
99+
dbServer := pkg.NewMcpServer(store)
100+
mcp.AddTool(server, &mcp.Tool{
101+
Name: "database-query",
102+
Description: "Query the database by SQL",
103+
}, dbServer.Query)
104+
105+
switch o.mode {
106+
case "sse":
107+
handler := mcp.NewSSEHandler(func(request *http.Request) *mcp.Server {
108+
return server
109+
})
110+
c.Println("Starting SSE server on port:", o.port)
111+
err = http.ListenAndServe(fmt.Sprintf(":%d", o.port), handler)
112+
case "stdio":
113+
err = server.Run(c.Context(), &mcp.StdioTransport{})
114+
case "http":
115+
fallthrough
116+
default:
117+
handler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server {
118+
return server
119+
}, nil)
120+
c.Println("Starting HTTP server on port:", o.port)
121+
err = http.ListenAndServe(fmt.Sprintf(":%d", o.port), handler)
122+
}
123+
return
124+
}

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ func NewRootCommand() (c *cobra.Command) {
3434
opt.AddFlags(c.Flags())
3535
c.Flags().IntVarP(&opt.historyLimit, "history-limit", "", 1000, "History record items count limit")
3636
c.Flags().BoolVarP(&opt.version, "version", "", false, "Print the version then exit")
37+
38+
c.AddCommand(newMCPCommand())
3739
return
3840
}
3941

e2e/compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ services:
2929
context: ..
3030
dockerfile: Dockerfile
3131
args:
32-
- "GO_BUILDER=ghcr.io/linuxsuren/library/golang:1.22"
32+
- "GO_BUILDER=golang:1.23"
3333
- "BASE_IMAGE=ghcr.io/linuxsuren/library/alpine:3.12"
3434
- GOPROXY=${GOPROXY}
3535
# ports:

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
module github.com/linuxsuren/atest-ext-store-orm
22

3-
go 1.22.4
3+
go 1.23.0
44

5-
toolchain go1.22.6
5+
toolchain go1.24.3
66

77
require (
88
github.com/linuxsuren/api-testing v0.0.20-0.20250319020913-f5f9383e2948
9+
github.com/modelcontextprotocol/go-sdk v0.3.1
910
github.com/spf13/cobra v1.8.1
1011
github.com/stretchr/testify v1.9.0
1112
github.com/taosdata/driver-go/v3 v3.6.0
@@ -43,6 +44,7 @@ require (
4344
github.com/go-openapi/swag v0.23.0 // indirect
4445
github.com/go-sql-driver/mysql v1.7.0 // indirect
4546
github.com/golang/protobuf v1.5.4 // indirect
47+
github.com/google/jsonschema-go v0.2.1-0.20250825175020-748c325cec76 // indirect
4648
github.com/google/uuid v1.6.0 // indirect
4749
github.com/gorilla/mux v1.8.1 // indirect
4850
github.com/gorilla/websocket v1.5.0 // indirect
@@ -89,6 +91,7 @@ require (
8991
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
9092
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
9193
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
94+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
9295
go.uber.org/multierr v1.11.0 // indirect
9396
go.uber.org/zap v1.27.0 // indirect
9497
golang.org/x/crypto v0.31.0 // indirect

go.sum

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
5151
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
5252
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
5353
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
54-
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
55-
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
54+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
55+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
5656
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
57+
github.com/google/jsonschema-go v0.2.1-0.20250825175020-748c325cec76 h1:mBlBwtDebdDYr+zdop8N62a44g+Nbv7o2KjWyS1deR4=
58+
github.com/google/jsonschema-go v0.2.1-0.20250825175020-748c325cec76/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
5759
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5860
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5961
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -120,6 +122,8 @@ github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HK
120122
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
121123
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
122124
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
125+
github.com/modelcontextprotocol/go-sdk v0.3.1 h1:0z04yIPlSwTluuelCBaL+wUag4YeflIU2Fr4Icb7M+o=
126+
github.com/modelcontextprotocol/go-sdk v0.3.1/go.mod h1:whv0wHnsTphwq7CTiKYHkLtwLC06WMoY2KpO+RB9yXQ=
123127
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
124128
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
125129
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -197,6 +201,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
197201
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
198202
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
199203
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
204+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
205+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
200206
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
201207
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
202208
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
@@ -248,6 +254,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
248254
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
249255
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
250256
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
257+
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
258+
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
251259
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
252260
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
253261
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=

pkg/data_query.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func runMultilineSQL(ctx context.Context, multilineSQL string, db *gorm.DB) (res
120120
}
121121

122122
func sqlQuery(ctx context.Context, sqlText string, db *gorm.DB) (result *server.DataQueryResult, err error) {
123+
fmt.Println("execute sql:", sqlText)
123124
var rows *sql.Rows
124125
if rows, err = db.Raw(sqlText).Rows(); err != nil {
125126
return
@@ -301,14 +302,16 @@ func (q *commonDataQuery) GetCurrentDatabase() (current string, err error) {
301302

302303
func (q *commonDataQuery) GetLabels(ctx context.Context, sql string) (metadata []*server.Pair) {
303304
metadata = make([]*server.Pair, 0)
304-
if databaseResult, err := sqlQuery(ctx, fmt.Sprintf("explain %s", sql), q.db); err == nil && len(databaseResult.Items) != 1 {
305-
for _, data := range databaseResult.Items[0].Data {
306-
switch data.Key {
307-
case "type":
308-
metadata = append(metadata, &server.Pair{
309-
Key: "sql_type",
310-
Value: data.Value,
311-
})
305+
if !strings.Contains(sql, ";") {
306+
if databaseResult, err := sqlQuery(ctx, fmt.Sprintf("explain %s", sql), q.db); err == nil && len(databaseResult.Items) != 1 {
307+
for _, data := range databaseResult.Items[0].Data {
308+
switch data.Key {
309+
case "type":
310+
metadata = append(metadata, &server.Pair{
311+
Key: "sql_type",
312+
Value: data.Value,
313+
})
314+
}
312315
}
313316
}
314317
}

0 commit comments

Comments
 (0)