package main import ( "bufio" "encoding/json" "fmt" "log" "os" "strings" "time" "github.com/rqlite/gorqlite" ) // MCP JSON-RPC types type JSONRPCRequest struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id,omitempty"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` } type JSONRPCResponse struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id"` Result any `json:"result,omitempty"` Error *ResponseError `json:"error,omitempty"` } type ResponseError struct { Code int `json:"code"` Message string `json:"message"` } // Tool definition type Tool struct { Name string `json:"name"` Description string `json:"description"` InputSchema any `json:"inputSchema"` } // Tool call types type CallToolRequest struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } type TextContent struct { Type string `json:"type"` Text string `json:"text"` } type CallToolResult struct { Content []TextContent `json:"content"` IsError bool `json:"isError,omitempty"` } type MCPServer struct { conn *gorqlite.Connection } func NewMCPServer(rqliteURL string) (*MCPServer, error) { conn, err := gorqlite.Open(rqliteURL) if err != nil { return nil, err } return &MCPServer{ conn: conn, }, nil } func (s *MCPServer) handleRequest(req JSONRPCRequest) JSONRPCResponse { var resp JSONRPCResponse resp.JSONRPC = "2.0" resp.ID = req.ID // Debug logging disabled to prevent excessive disk writes // log.Printf("Received method: %s", req.Method) switch req.Method { case "initialize": resp.Result = map[string]any{ "protocolVersion": "2024-11-05", "capabilities": map[string]any{ "tools": map[string]any{}, }, "serverInfo": map[string]any{ "name": "rqlite-mcp", "version": "0.1.0", }, } case "notifications/initialized": // This is a notification, no response needed return JSONRPCResponse{} case "tools/list": // Debug logging disabled to prevent excessive disk writes tools := []Tool{ { Name: "list_tables", Description: "List all tables in the Rqlite database", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{}, }, }, { Name: "query", Description: "Run a SELECT query on the Rqlite database", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "sql": map[string]any{ "type": "string", "description": "The SQL SELECT query to run", }, }, "required": []string{"sql"}, }, }, { Name: "execute", Description: "Run an INSERT, UPDATE, or DELETE statement on the Rqlite database", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "sql": map[string]any{ "type": "string", "description": "The SQL statement (INSERT, UPDATE, DELETE) to run", }, }, "required": []string{"sql"}, }, }, } resp.Result = map[string]any{"tools": tools} case "tools/call": var callReq CallToolRequest if err := json.Unmarshal(req.Params, &callReq); err != nil { resp.Error = &ResponseError{Code: -32700, Message: "Parse error"} return resp } resp.Result = s.handleToolCall(callReq) default: // Debug logging disabled to prevent excessive disk writes resp.Error = &ResponseError{Code: -32601, Message: "Method not found"} } return resp } func (s *MCPServer) handleToolCall(req CallToolRequest) CallToolResult { // Debug logging disabled to prevent excessive disk writes // log.Printf("Tool call: %s", req.Name) switch req.Name { case "list_tables": rows, err := s.conn.QueryOne("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") if err != nil { return errorResult(fmt.Sprintf("Error listing tables: %v", err)) } var tables []string for rows.Next() { slice, err := rows.Slice() if err == nil && len(slice) > 0 { tables = append(tables, fmt.Sprint(slice[0])) } } if len(tables) == 0 { return textResult("No tables found") } return textResult(strings.Join(tables, "\n")) case "query": var args struct { SQL string `json:"sql"` } if err := json.Unmarshal(req.Arguments, &args); err != nil { return errorResult(fmt.Sprintf("Invalid arguments: %v", err)) } // Debug logging disabled to prevent excessive disk writes rows, err := s.conn.QueryOne(args.SQL) if err != nil { return errorResult(fmt.Sprintf("Query error: %v", err)) } var result strings.Builder cols := rows.Columns() result.WriteString(strings.Join(cols, " | ") + "\n") result.WriteString(strings.Repeat("-", len(cols)*10) + "\n") rowCount := 0 for rows.Next() { vals, err := rows.Slice() if err != nil { continue } rowCount++ for i, v := range vals { if i > 0 { result.WriteString(" | ") } result.WriteString(fmt.Sprint(v)) } result.WriteString("\n") } result.WriteString(fmt.Sprintf("\n(%d rows)", rowCount)) return textResult(result.String()) case "execute": var args struct { SQL string `json:"sql"` } if err := json.Unmarshal(req.Arguments, &args); err != nil { return errorResult(fmt.Sprintf("Invalid arguments: %v", err)) } // Debug logging disabled to prevent excessive disk writes res, err := s.conn.WriteOne(args.SQL) if err != nil { return errorResult(fmt.Sprintf("Execution error: %v", err)) } return textResult(fmt.Sprintf("Rows affected: %d", res.RowsAffected)) default: return errorResult(fmt.Sprintf("Unknown tool: %s", req.Name)) } } func textResult(text string) CallToolResult { return CallToolResult{ Content: []TextContent{ { Type: "text", Text: text, }, }, } } func errorResult(text string) CallToolResult { return CallToolResult{ Content: []TextContent{ { Type: "text", Text: text, }, }, IsError: true, } } func main() { // Log to stderr so stdout is clean for JSON-RPC log.SetOutput(os.Stderr) rqliteURL := "http://localhost:5001" if u := os.Getenv("RQLITE_URL"); u != "" { rqliteURL = u } var server *MCPServer var err error // Retry connecting to rqlite maxRetries := 30 for i := 0; i < maxRetries; i++ { server, err = NewMCPServer(rqliteURL) if err == nil { break } if i%5 == 0 { log.Printf("Waiting for Rqlite at %s... (%d/%d)", rqliteURL, i+1, maxRetries) } time.Sleep(1 * time.Second) } if err != nil { log.Fatalf("Failed to connect to Rqlite after %d retries: %v", maxRetries, err) } log.Printf("MCP Rqlite server started (stdio transport)") log.Printf("Connected to Rqlite at %s", rqliteURL) // Read JSON-RPC requests from stdin, write responses to stdout scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { line := scanner.Text() if line == "" { continue } var req JSONRPCRequest if err := json.Unmarshal([]byte(line), &req); err != nil { // Debug logging disabled to prevent excessive disk writes continue } resp := server.handleRequest(req) // Don't send response for notifications (no ID) if req.ID == nil && strings.HasPrefix(req.Method, "notifications/") { continue } respData, err := json.Marshal(resp) if err != nil { // Debug logging disabled to prevent excessive disk writes continue } fmt.Println(string(respData)) } if err := scanner.Err(); err != nil { // Debug logging disabled to prevent excessive disk writes } }