Skip to main content
Glama
googleapis

MCP Toolbox for Databases

by googleapis
mindsdb_integration_test.go21.4 kB
// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mindsdb import ( "bytes" "context" "database/sql" "fmt" "net/http" "os" "regexp" "strings" "testing" "time" _ "github.com/go-sql-driver/mysql" "github.com/google/uuid" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/tests" ) var ( MindsDBSourceKind = "mindsdb" MindsDBToolKind = "mindsdb-sql" MindsDBDatabase = os.Getenv("MINDSDB_DATABASE") MindsDBHost = os.Getenv("MINDSDB_HOST") MindsDBPort = os.Getenv("MINDSDB_PORT") MindsDBUser = os.Getenv("MINDSDB_USER") MindsDBPass = os.Getenv("MINDSDB_PASS") ) func getMindsDBVars(t *testing.T) map[string]any { switch "" { case MindsDBDatabase: t.Fatal("'MINDSDB_DATABASE' not set") case MindsDBHost: t.Fatal("'MINDSDB_HOST' not set") case MindsDBPort: t.Fatal("'MINDSDB_PORT' not set") case MindsDBUser: t.Fatal("'MINDSDB_USER' not set") } // MindsDBPass can be empty, but the env var must exist if _, exists := os.LookupEnv("MINDSDB_PASS"); !exists { t.Fatal("'MINDSDB_PASS' not set (can be empty)") } // Handle no-password authentication mindsdbPassword := MindsDBPass if mindsdbPassword == "none" { mindsdbPassword = "" } return map[string]any{ "kind": MindsDBSourceKind, "host": MindsDBHost, "port": MindsDBPort, "database": MindsDBDatabase, "user": MindsDBUser, "password": mindsdbPassword, } } // initMindsDBConnectionPool creates a connection pool using MySQL protocol func initMindsDBConnectionPool(host, port, user, pass, dbname string) (*sql.DB, error) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname) pool, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("sql.Open: %w", err) } return pool, nil } func TestMindsDBToolEndpoints(t *testing.T) { sourceConfig := getMindsDBVars(t) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() var args []string // Create unique table names with UUID tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") // Tool statements with ORDER BY for consistent results paramToolStmt := fmt.Sprintf("SELECT * FROM files.%s WHERE id = ? OR name = ? ORDER BY id", tableNameParam) idParamToolStmt := fmt.Sprintf("SELECT * FROM files.%s WHERE id = ? ORDER BY id", tableNameParam) nameParamToolStmt := fmt.Sprintf("SELECT * FROM files.%s WHERE name = ? ORDER BY id", tableNameParam) authToolStmt := fmt.Sprintf("SELECT name FROM files.%s WHERE email = ? ORDER BY name", tableNameAuth) toolsFile := map[string]any{ "sources": map[string]any{ "my-instance": sourceConfig, }, "authServices": map[string]any{ "my-google-auth": map[string]any{ "kind": "google", "clientId": tests.ClientId, }, }, "tools": map[string]any{ "my-simple-tool": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Simple tool to test end to end functionality.", "statement": "SELECT 1", }, "my-tool": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test invocation with params.", "statement": paramToolStmt, "parameters": []map[string]any{ { "name": "id", "type": "integer", "description": "user ID", }, { "name": "name", "type": "string", "description": "user name", }, }, }, "my-tool-by-id": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test invocation with params.", "statement": idParamToolStmt, "parameters": []map[string]any{ { "name": "id", "type": "integer", "description": "user ID", }, }, }, "my-tool-by-name": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test invocation with params.", "statement": nameParamToolStmt, "parameters": []map[string]any{ { "name": "name", "type": "string", "description": "user name", "required": false, }, }, }, "my-array-tool": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test invocation with array params.", "statement": "SELECT 1 as id, 'Alice' as name UNION SELECT 3 as id, 'Sid' as name", }, "my-auth-tool": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test authenticated parameters.", "statement": authToolStmt, "parameters": []map[string]any{ { "name": "email", "type": "string", "description": "user email", "authServices": []map[string]string{ { "name": "my-google-auth", "field": "email", }, }, }, }, }, "my-auth-required-tool": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test auth required invocation.", "statement": "SELECT 1", "authRequired": []string{ "my-google-auth", }, }, "my-fail-tool": map[string]any{ "kind": MindsDBToolKind, "source": "my-instance", "description": "Tool to test statement with incorrect syntax.", "statement": "INVALID SQL STATEMENT", }, "my-exec-sql-tool": map[string]any{ "kind": "mindsdb-execute-sql", "source": "my-instance", "description": "Tool to execute sql", }, "my-auth-exec-sql-tool": map[string]any{ "kind": "mindsdb-execute-sql", "source": "my-instance", "description": "Tool to execute sql with auth", "authRequired": []string{ "my-google-auth", }, }, }, } cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) } defer cleanup() waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) if err != nil { t.Logf("toolbox command logs: \n%s", out) t.Fatalf("toolbox didn't start successfully: %s", err) } // Create connection pool and test tables with sample data pool, err := initMindsDBConnectionPool(MindsDBHost, MindsDBPort, MindsDBUser, MindsDBPass, MindsDBDatabase) if err != nil { t.Fatalf("unable to create MindsDB connection pool: %s", err) } defer pool.Close() // Create param table: id=1:Alice, id=2:Jane, id=3:Sid, id=4:null createParamSQL := fmt.Sprintf("CREATE TABLE files.%s (SELECT 1 as id, 'Alice' as name UNION ALL SELECT 2, 'Jane' UNION ALL SELECT 3, 'Sid' UNION ALL SELECT 4, NULL)", tableNameParam) _, err = pool.ExecContext(ctx, createParamSQL) if err != nil { t.Fatalf("unable to create param table: %s", err) } // Create auth table: id=1:Alice:test@..., id=2:Jane:jane@... createAuthSQL := fmt.Sprintf("CREATE TABLE files.%s (SELECT 1 as id, 'Alice' as name, '%s' as email UNION ALL SELECT 2, 'Jane', 'janedoe@gmail.com')", tableNameAuth, tests.ServiceAccountEmail) _, err = pool.ExecContext(ctx, createAuthSQL) if err != nil { t.Fatalf("unable to create auth table: %s", err) } defer func() { _, _ = pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS files.%s", tableNameParam)) _, _ = pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS files.%s", tableNameAuth)) }() select1Want := "[{\"1\":1}]" // Run standard tool tests with MindsDB-specific expectations tests.RunToolGetTest(t) tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest(), // MindsDB doesn't support array parameters ) t.Run("mindsdb_core_functionality", func(t *testing.T) { tests.RunToolInvokeSimpleTest(t, "my-simple-tool", select1Want) tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1"}`), select1Want) tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1+1 as result"}`), "[{\"result\":2}]") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 'hello' as greeting"}`), "[{\"greeting\":\"hello\"}]") }) t.Run("mindsdb_sql_tests", func(t *testing.T) { tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1"}`), select1Want) tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW DATABASES"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW TABLES"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT TABLE_NAME FROM information_schema.TABLES LIMIT 1"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1+1 as result"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT UPPER('hello') as result"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT NOW() as current_time"}`), "") }) // Test CREATE DATABASE (MindsDB's federated database capability) t.Run("mindsdb_create_database", func(t *testing.T) { tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP DATABASE IF EXISTS test_postgres_db"}`), "") // Create external database integration using MindsDB's demo database createDBSQL := `CREATE DATABASE test_postgres_db WITH ENGINE = 'postgres', PARAMETERS = {'user': 'demo_user', 'password': 'demo_password', 'host': 'samples.mindsdb.com', 'port': '5432', 'database': 'demo', 'schema': 'demo_data'}` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+createDBSQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW DATABASES"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW TABLES FROM test_postgres_db"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP DATABASE IF EXISTS test_postgres_db"}`), "") }) // Test MindsDB integration capabilities with product/review data t.Run("mindsdb_integration_demo", func(t *testing.T) { tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_products"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_reviews"}`), "") // Create test tables with sample data createProductsSQL := `CREATE TABLE files.test_products (SELECT 'PROD001' as product_id, 'Laptop Computer' as product_name, 'Electronics' as category UNION ALL SELECT 'PROD002', 'Office Chair', 'Furniture' UNION ALL SELECT 'PROD003', 'Coffee Maker', 'Appliances' UNION ALL SELECT 'PROD004', 'Desk Lamp', 'Furniture')` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+createProductsSQL+`"}`), "") createReviewsSQL := `CREATE TABLE files.test_reviews (SELECT 'PROD001' as product_id, 'Great laptop, very fast!' as review, 5 as rating UNION ALL SELECT 'PROD001', 'Good value for money', 4 UNION ALL SELECT 'PROD002', 'Very comfortable chair', 5 UNION ALL SELECT 'PROD002', 'Nice design but expensive', 3 UNION ALL SELECT 'PROD003', 'Makes excellent coffee', 5 UNION ALL SELECT 'PROD004', 'Bright light, perfect for reading', 4)` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+createReviewsSQL+`"}`), "") t.Run("query_created_tables", func(t *testing.T) { tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT * FROM files.test_products ORDER BY product_id"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT * FROM files.test_reviews ORDER BY product_id, rating DESC"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT category, COUNT(*) as product_count FROM files.test_products GROUP BY category ORDER BY category"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT product_id, AVG(rating) as avg_rating FROM files.test_reviews GROUP BY product_id ORDER BY avg_rating DESC"}`), "") }) t.Run("cross_database_join", func(t *testing.T) { joinSQL := `SELECT p.product_name, p.category, r.review, r.rating FROM files.test_products p JOIN files.test_reviews r ON p.product_id = r.product_id WHERE r.rating >= 4 ORDER BY p.product_name, r.rating DESC` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+joinSQL+`"}`), "") aggSQL := `SELECT p.category, COUNT(DISTINCT p.product_id) as product_count, COUNT(r.review) as review_count, AVG(r.rating) as avg_rating FROM files.test_products p LEFT JOIN files.test_reviews r ON p.product_id = r.product_id GROUP BY p.category ORDER BY avg_rating DESC` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+aggSQL+`"}`), "") }) t.Run("advanced_sql_features", func(t *testing.T) { subquerySQL := `SELECT p.product_name, p.category, AVG(r.rating) as avg_rating FROM files.test_products p JOIN files.test_reviews r ON p.product_id = r.product_id GROUP BY p.product_id, p.product_name, p.category HAVING AVG(r.rating) >= 4 ORDER BY avg_rating DESC` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+subquerySQL+`"}`), "") caseSQL := `SELECT product_id, review, rating, CASE WHEN rating >= 5 THEN 'Excellent' WHEN rating >= 4 THEN 'Good' WHEN rating >= 3 THEN 'Average' ELSE 'Poor' END as rating_category FROM files.test_reviews ORDER BY rating DESC, product_id` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+caseSQL+`"}`), "") }) tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_product_summary"}`), "") t.Run("data_manipulation", func(t *testing.T) { summarySQL := `CREATE TABLE files.test_product_summary (SELECT p.product_id, p.product_name, p.category, COUNT(r.review) as total_reviews, AVG(r.rating) as avg_rating, MAX(r.rating) as max_rating, MIN(r.rating) as min_rating FROM files.test_products p LEFT JOIN files.test_reviews r ON p.product_id = r.product_id GROUP BY p.product_id, p.product_name, p.category)` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+summarySQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT * FROM files.test_product_summary ORDER BY avg_rating DESC"}`), "") }) tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_products"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_reviews"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_product_summary"}`), "") }) // Test database integration and cross-database joins t.Run("mindsdb_create_database_integration", func(t *testing.T) { showDBSQL := `SHOW DATABASES` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+showDBSQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW TABLES FROM files"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_integration_data"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_local_data"}`), "") createIntegrationTableSQL := `CREATE TABLE files.test_integration_data (SELECT 1 as id, 'Data from integration' as description, CURDATE() as created_at UNION ALL SELECT 2, 'Another record', CURDATE() UNION ALL SELECT 3, 'Third record', CURDATE())` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+createIntegrationTableSQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT * FROM files.test_integration_data ORDER BY id"}`), "") createLocalTableSQL := `CREATE TABLE files.test_local_data (SELECT 1 as id, 'Local metadata' as metadata UNION ALL SELECT 2, 'More metadata')` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+createLocalTableSQL+`"}`), "") crossJoinSQL := `SELECT i.id, i.description, l.metadata FROM files.test_integration_data i LEFT JOIN files.test_local_data l ON i.id = l.id ORDER BY i.id` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+crossJoinSQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_integration_data"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_local_data"}`), "") }) // Test data transformation with customer order data t.Run("mindsdb_data_transformation", func(t *testing.T) { tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_orders"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_customer_summary"}`), "") createOrdersSQL := `CREATE TABLE files.test_orders (SELECT 1 as order_id, 'CUST001' as customer_id, 100.50 as amount, '2024-01-15' as order_date UNION ALL SELECT 2, 'CUST001', 250.00, '2024-02-20' UNION ALL SELECT 3, 'CUST002', 75.25, '2024-01-18' UNION ALL SELECT 4, 'CUST003', 500.00, '2024-03-10' UNION ALL SELECT 5, 'CUST002', 150.00, '2024-02-25')` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+createOrdersSQL+`"}`), "") customerSummarySQL := `CREATE TABLE files.test_customer_summary (SELECT customer_id, COUNT(*) as total_orders, SUM(amount) as total_spent, AVG(amount) as avg_order_value, MIN(order_date) as first_order_date, MAX(order_date) as last_order_date FROM files.test_orders GROUP BY customer_id)` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+customerSummarySQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT * FROM files.test_customer_summary ORDER BY total_spent DESC"}`), "") segmentSQL := `SELECT customer_id, total_spent, CASE WHEN total_spent >= 300 THEN 'High Value' WHEN total_spent >= 150 THEN 'Medium Value' ELSE 'Low Value' END as customer_segment FROM files.test_customer_summary ORDER BY total_spent DESC` tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "`+segmentSQL+`"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_orders"}`), "") tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "DROP TABLE IF EXISTS files.test_customer_summary"}`), "") }) // Test error handling - these are expected to fail but exercise error paths t.Run("mindsdb_error_handling", func(t *testing.T) { // Test invalid SQL - expect this to fail with 400 resp, err := http.Post("http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke", "application/json", bytes.NewBuffer([]byte(`{"sql": "INVALID SQL QUERY"}`))) if err != nil { t.Fatalf("error when sending request: %s", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Logf("Expected 400 for invalid SQL, got %d (this exercises error handling)", resp.StatusCode) } // Test empty SQL - expect this to fail with 400 resp2, err := http.Post("http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke", "application/json", bytes.NewBuffer([]byte(`{"sql": ""}`))) if err != nil { t.Fatalf("error when sending request: %s", err) } defer resp2.Body.Close() if resp2.StatusCode != http.StatusBadRequest { t.Logf("Expected 400 for empty SQL, got %d (this exercises error handling)", resp2.StatusCode) } }) // Test authentication - these are expected to fail but exercise auth code paths t.Run("mindsdb_auth_tests", func(t *testing.T) { // Test auth-required tool without auth - expect this to fail with 401 resp, err := http.Post("http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke", "application/json", bytes.NewBuffer([]byte(`{"sql": "SELECT 1"}`))) if err != nil { t.Fatalf("error when sending request: %s", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Logf("Expected 401 for missing auth, got %d (this exercises auth handling)", resp.StatusCode) } }) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/googleapis/genai-toolbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server