Skip to content

Commit e2a34e0

Browse files
authored
Merge pull request #9716 from dolthub/ca/dolt-sql-server-mcp
add option to start an http mcp server when dolt sql-server is running
2 parents 7239c56 + 676778e commit e2a34e0

File tree

11 files changed

+925
-49
lines changed

11 files changed

+925
-49
lines changed

go/Godeps/LICENSES

Lines changed: 299 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright 2025 Dolthub, Inc.
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 sqlserver
16+
17+
import (
18+
"context"
19+
"net"
20+
"strconv"
21+
22+
"github.com/sirupsen/logrus"
23+
"go.uber.org/zap"
24+
"go.uber.org/zap/zapcore"
25+
"golang.org/x/sync/errgroup"
26+
27+
pkgmcp "github.com/dolthub/dolt-mcp/mcp/pkg"
28+
mcpdb "github.com/dolthub/dolt-mcp/mcp/pkg/db"
29+
"github.com/dolthub/dolt-mcp/mcp/pkg/toolsets"
30+
"github.com/dolthub/dolt/go/libraries/utils/svcs"
31+
)
32+
33+
// MCPConfig encapsulates MCP-specific configuration for the sql-server
34+
type MCPConfig struct {
35+
Port *int
36+
User *string
37+
Password *string
38+
Database *string
39+
}
40+
41+
// logrusZapCore implements a zapcore.Core that forwards entries to a logrus.Logger
42+
type logrusZapCore struct {
43+
l *logrus.Logger
44+
prefix string
45+
}
46+
47+
func newZapFromLogrusWithPrefix(l *logrus.Logger, prefix string) *zap.Logger {
48+
core := &logrusZapCore{l: l, prefix: prefix}
49+
return zap.New(core, zap.AddCallerSkip(1))
50+
}
51+
52+
func (c *logrusZapCore) With(fields []zapcore.Field) zapcore.Core {
53+
// zap will call Write with fields; we don't need to carry state here
54+
return c
55+
}
56+
57+
func (c *logrusZapCore) Enabled(lvl zapcore.Level) bool {
58+
// Respect logrus current level
59+
return zapToLogrusLevel(lvl) >= c.l.GetLevel()
60+
}
61+
62+
func (c *logrusZapCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
63+
if c.Enabled(ent.Level) {
64+
return ce.AddCore(ent, c)
65+
}
66+
return ce
67+
}
68+
69+
func (c *logrusZapCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
70+
enc := zapcore.NewMapObjectEncoder()
71+
for _, f := range fields {
72+
f.AddTo(enc)
73+
}
74+
// Build fields map
75+
lf := logrus.Fields(enc.Fields)
76+
msg := c.prefix + ent.Message
77+
c.l.WithFields(lf).Log(zapToLogrusLevel(ent.Level), msg)
78+
return nil
79+
}
80+
81+
func (c *logrusZapCore) Sync() error { return nil }
82+
83+
func zapToLogrusLevel(l zapcore.Level) logrus.Level {
84+
switch l {
85+
case zapcore.DebugLevel:
86+
return logrus.DebugLevel
87+
case zapcore.InfoLevel:
88+
return logrus.InfoLevel
89+
case zapcore.WarnLevel:
90+
return logrus.WarnLevel
91+
case zapcore.ErrorLevel:
92+
return logrus.ErrorLevel
93+
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
94+
return logrus.FatalLevel
95+
default:
96+
return logrus.InfoLevel
97+
}
98+
}
99+
100+
// mcpInit validates and reserves the MCP port
101+
func mcpInit(cfg *Config, state *svcs.ServiceState) func(context.Context) error {
102+
return func(context.Context) error {
103+
addr := net.JoinHostPort("0.0.0.0", strconv.Itoa(*cfg.MCP.Port))
104+
l, err := net.Listen("tcp", addr)
105+
if err != nil {
106+
return err
107+
}
108+
_ = l.Close()
109+
state.Swap(svcs.ServiceState_Init)
110+
return nil
111+
}
112+
}
113+
114+
// mcpRun starts the MCP HTTP server and wires lifecycle to errgroup
115+
func mcpRun(cfg *Config, lgr *logrus.Logger, state *svcs.ServiceState, cancelPtr *context.CancelFunc, groupPtr **errgroup.Group) func(context.Context) {
116+
return func(ctx context.Context) {
117+
if !state.CompareAndSwap(svcs.ServiceState_Init, svcs.ServiceState_Run) {
118+
return
119+
}
120+
121+
// Logger for MCP (prefix and level inherited from logrus)
122+
logger := newZapFromLogrusWithPrefix(lgr, "[dolt-mcp] ")
123+
124+
if cfg.MCP.User == nil || *cfg.MCP.User == "" {
125+
lgr.WithField("component", "dolt-mcp").Error("MCP user not defined")
126+
return
127+
}
128+
129+
// Build DB config
130+
password := ""
131+
if cfg.MCP.Password != nil {
132+
password = *cfg.MCP.Password
133+
}
134+
dbName := ""
135+
if cfg.MCP.Database != nil {
136+
dbName = *cfg.MCP.Database
137+
}
138+
dbConf := mcpdb.Config{
139+
Host: "127.0.0.1",
140+
Port: cfg.ServerConfig.Port(),
141+
User: *cfg.MCP.User,
142+
Password: password,
143+
DatabaseName: dbName,
144+
}
145+
146+
srv, err := pkgmcp.NewMCPHTTPServer(
147+
logger,
148+
dbConf,
149+
*cfg.MCP.Port,
150+
toolsets.WithToolSet(&toolsets.PrimitiveToolSetV1{}),
151+
)
152+
if err != nil {
153+
lgr.WithField("component", "dolt-mcp").Errorf("failed to start Dolt MCP HTTP server: %v", err)
154+
return
155+
}
156+
157+
runCtx, cancel := context.WithCancel(ctx)
158+
*cancelPtr = cancel
159+
g, gctx := errgroup.WithContext(runCtx)
160+
g.Go(func() error { srv.ListenAndServe(gctx); return nil })
161+
*groupPtr = g
162+
}
163+
}
164+
165+
// mcpStop gracefully stops the MCP server by cancelling context and waiting for the errgroup
166+
func mcpStop(cancel context.CancelFunc, group *errgroup.Group, state *svcs.ServiceState) func() error {
167+
return func() error {
168+
if cancel != nil {
169+
cancel()
170+
}
171+
if group != nil {
172+
if err := group.Wait(); err != nil {
173+
return err
174+
}
175+
}
176+
state.Swap(svcs.ServiceState_Stopped)
177+
return nil
178+
}
179+
}
180+
181+
// registerMCPService wires the MCP service into the controller using helper funcs
182+
func registerMCPService(controller *svcs.Controller, cfg *Config, lgr *logrus.Logger) {
183+
if cfg.MCP == nil || cfg.MCP.Port == nil || *cfg.MCP.Port <= 0 {
184+
return
185+
}
186+
var state svcs.ServiceState
187+
var cancel context.CancelFunc
188+
var group *errgroup.Group
189+
190+
svc := &svcs.AnonService{
191+
InitF: mcpInit(cfg, &state),
192+
RunF: mcpRun(cfg, lgr, &state, &cancel, &group),
193+
StopF: mcpStop(cancel, group, &state),
194+
}
195+
_ = controller.Register(svc)
196+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2025 Dolthub, Inc.
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 sqlserver
16+
17+
import (
18+
"fmt"
19+
"net"
20+
"os"
21+
"strconv"
22+
23+
"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
24+
"github.com/dolthub/dolt/go/libraries/doltcore/servercfg"
25+
)
26+
27+
// validateAndPrepareMCP performs coherence checks for MCP options and fails fast
28+
// when the specified port is unavailable. It also sets DOLT_ROOT_HOST dynamically
29+
// when MCP is enabled and the env var is not already set.
30+
func validateAndPrepareMCP(
31+
serverConfig servercfg.ServerConfig,
32+
mcpPort *int,
33+
mcpUser *string,
34+
mcpPassword *string,
35+
mcpDatabase *string,
36+
) error {
37+
// If any MCP-related arg is supplied without --mcp-port, error
38+
if mcpPort == nil {
39+
if mcpUser != nil && *mcpUser != "" {
40+
return fmt.Errorf("--%s requires --%s to be set", mcpUserFlag, mcpPortFlag)
41+
}
42+
if mcpPassword != nil && *mcpPassword != "" {
43+
return fmt.Errorf("--%s requires --%s to be set", mcpPasswordFlag, mcpPortFlag)
44+
}
45+
if mcpDatabase != nil && *mcpDatabase != "" {
46+
return fmt.Errorf("--%s requires --%s to be set", mcpDatabaseFlag, mcpPortFlag)
47+
}
48+
return nil
49+
}
50+
51+
// --mcp-user is REQUIRED when --mcp-port is provided
52+
if mcpUser == nil || *mcpUser == "" {
53+
return fmt.Errorf("--%s is required when --%s is specified", mcpUserFlag, mcpPortFlag)
54+
}
55+
56+
// Range and conflict checks
57+
if *mcpPort <= 0 || *mcpPort > 65535 {
58+
return fmt.Errorf("invalid value for --%s '%d'", mcpPortFlag, *mcpPort)
59+
}
60+
if serverConfig.Port() == *mcpPort {
61+
return fmt.Errorf("--%s must differ from --%s (both set to %d)", mcpPortFlag, portFlag, *mcpPort)
62+
}
63+
64+
// If MCP is enabled and no explicit root host override exists, set DOLT_ROOT_HOST dynamically
65+
// to the SQL listener host, defaulting to 127.0.0.1 when unspecified or wildcard.
66+
if _, ok := os.LookupEnv(dconfig.EnvDoltRootHost); !ok {
67+
hostForRoot := serverConfig.Host()
68+
if hostForRoot == "" || hostForRoot == "0.0.0.0" || hostForRoot == "::" {
69+
hostForRoot = "127.0.0.1"
70+
}
71+
_ = os.Setenv(dconfig.EnvDoltRootHost, hostForRoot)
72+
}
73+
74+
// Preflight port availability to fail fast before starting controller
75+
addr := net.JoinHostPort("0.0.0.0", strconv.Itoa(*mcpPort))
76+
l, err := net.Listen("tcp", addr)
77+
if err != nil {
78+
return fmt.Errorf("MCP port %d already in use: %v", *mcpPort, err)
79+
}
80+
_ = l.Close()
81+
return nil
82+
}

go/cmd/dolt/commands/sqlserver/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type Config struct {
8484
Version string
8585
Controller *svcs.Controller
8686
ProtocolListenerFactory server.ProtocolListenerFunc
87+
MCP *MCPConfig
8788
}
8889

8990
// Serve starts a MySQL-compatible server. Returns any errors that were encountered.
@@ -866,6 +867,9 @@ func ConfigureServices(
866867
},
867868
}
868869
controller.Register(RunSQLServer)
870+
871+
// Optionally start an MCP HTTP server
872+
registerMCPService(controller, cfg, lgr)
869873
}
870874

871875
// heartbeatService is a service that sends a heartbeat event to the metrics server once a day

go/cmd/dolt/commands/sqlserver/sqlserver.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const (
5656
remotesapiReadOnlyFlag = "remotesapi-readonly"
5757
goldenMysqlConn = "golden"
5858
eventSchedulerStatus = "event-scheduler"
59+
mcpPortFlag = "mcp-port"
60+
mcpUserFlag = "mcp-user"
61+
mcpPasswordFlag = "mcp-password"
62+
mcpDatabaseFlag = "mcp-database"
5963
)
6064

6165
func indentLines(s string) string {
@@ -195,6 +199,12 @@ func (cmd SqlServerCmd) ArgParserWithName(name string) *argparser.ArgParser {
195199
ap.SupportsFlag(remotesapiReadOnlyFlag, "", "Disable writes to the sql-server via the push operations. SQL writes are unaffected by this setting.")
196200
ap.SupportsString(goldenMysqlConn, "", "mysql connection string", "Provides a connection string to a MySQL instance to be used to validate query results")
197201
ap.SupportsString(eventSchedulerStatus, "", "status", "Determines whether the Event Scheduler is enabled and running on the server. It has one of the following values: 'ON', 'OFF' or 'DISABLED'.")
202+
// Start an MCP HTTP server connected to this sql-server on the given port
203+
ap.SupportsUint(mcpPortFlag, "", "port", "If provided, runs a Dolt MCP HTTP server on this port alongside the sql-server.")
204+
// MCP SQL credentials (user required when MCP enabled; password optional)
205+
ap.SupportsString(mcpUserFlag, "", "user", "SQL user for MCP to connect as (required when --mcp-port is set).")
206+
ap.SupportsString(mcpPasswordFlag, "", "password", "Optional SQL password for MCP to connect with (requires --mcp-user). Defaults to env DOLT_ROOT_PASSWORD if unset.")
207+
ap.SupportsString(mcpDatabaseFlag, "", "database", "Optional SQL database name MCP should connect to (requires --mcp-port and --mcp-user).")
198208
return ap
199209
}
200210

@@ -266,6 +276,37 @@ func StartServer(ctx context.Context, versionStr, commandStr string, args []stri
266276
return err
267277
}
268278

279+
// Optional MCP HTTP port
280+
var mcpPortPtr *int
281+
if mp, ok := apr.GetInt(mcpPortFlag); ok {
282+
mcpPort := mp
283+
mcpPortPtr = &mcpPort
284+
}
285+
286+
// Optional MCP SQL user
287+
var mcpUserPtr *string
288+
if mu, ok := apr.GetValue(mcpUserFlag); ok {
289+
user := mu
290+
mcpUserPtr = &user
291+
}
292+
// Optional MCP SQL password
293+
var mcpPasswordPtr *string
294+
if mpw, ok := apr.GetValue(mcpPasswordFlag); ok {
295+
pw := mpw
296+
mcpPasswordPtr = &pw
297+
}
298+
// Optional MCP SQL database
299+
var mcpDatabasePtr *string
300+
if mdb, ok := apr.GetValue(mcpDatabaseFlag); ok {
301+
db := mdb
302+
mcpDatabasePtr = &db
303+
}
304+
305+
// Validate and prepare MCP options in dedicated helper
306+
if err := validateAndPrepareMCP(serverConfig, mcpPortPtr, mcpUserPtr, mcpPasswordPtr, mcpDatabasePtr); err != nil {
307+
return err
308+
}
309+
269310
err = generateYamlConfigIfNone(ap, help, args, dEnv, serverConfig)
270311
if err != nil {
271312
return err
@@ -278,13 +319,26 @@ func StartServer(ctx context.Context, versionStr, commandStr string, args []stri
278319

279320
cli.Printf("Starting server with Config %v\n", servercfg.ConfigInfo(serverConfig))
280321

322+
// Build MCP config if any MCP-related options are present
323+
var mcpCfg *MCPConfig
324+
if mcpPortPtr != nil || (mcpUserPtr != nil && *mcpUserPtr != "") || (mcpPasswordPtr != nil && *mcpPasswordPtr != "") || (mcpDatabasePtr != nil && *mcpDatabasePtr != "") {
325+
mcpCfg = &MCPConfig{
326+
Port: mcpPortPtr,
327+
User: mcpUserPtr,
328+
Password: mcpPasswordPtr,
329+
Database: mcpDatabasePtr,
330+
}
331+
}
332+
281333
skipRootUserInitialization := apr.Contains(skipRootUserInitialization)
334+
282335
startError, closeError := Serve(ctx, &Config{
283336
Version: versionStr,
284337
ServerConfig: serverConfig,
285338
Controller: controller,
286339
DoltEnv: dEnv,
287340
SkipRootUserInit: skipRootUserInitialization,
341+
MCP: mcpCfg,
288342
})
289343
if startError != nil {
290344
return startError

0 commit comments

Comments
 (0)