Skip to main content
Glama

MCP Toolbox for Databases

by googleapis
Apache 2.0
11,037
  • Linux
firebird_integration_test.go17.7 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 firebird import ( "context" "database/sql" "fmt" "os" "regexp" "strings" "testing" "time" "github.com/google/uuid" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/tests" _ "github.com/nakagami/firebirdsql" ) var ( FirebirdSourceKind = "firebird" FirebirdToolKind = "firebird-sql" FirebirdDatabase = os.Getenv("FIREBIRD_DATABASE") FirebirdHost = os.Getenv("FIREBIRD_HOST") FirebirdPort = os.Getenv("FIREBIRD_PORT") FirebirdUser = os.Getenv("FIREBIRD_USER") FirebirdPass = os.Getenv("FIREBIRD_PASS") ) func getFirebirdVars(t *testing.T) map[string]any { switch "" { case FirebirdDatabase: t.Fatal("'FIREBIRD_DATABASE' not set") case FirebirdHost: t.Fatal("'FIREBIRD_HOST' not set") case FirebirdPort: t.Fatal("'FIREBIRD_PORT' not set") case FirebirdUser: t.Fatal("'FIREBIRD_USER' not set") case FirebirdPass: t.Fatal("'FIREBIRD_PASS' not set") } return map[string]any{ "kind": FirebirdSourceKind, "host": FirebirdHost, "port": FirebirdPort, "database": FirebirdDatabase, "user": FirebirdUser, "password": FirebirdPass, } } func initFirebirdConnection(host, port, user, pass, dbname string) (*sql.DB, error) { dsn := fmt.Sprintf("%s:%s@%s:%s/%s", user, pass, host, port, dbname) db, err := sql.Open("firebirdsql", dsn) if err != nil { return nil, fmt.Errorf("unable to create connection pool: %w", err) } // Configure connection pool to prevent deadlocks db.SetMaxOpenConns(5) db.SetMaxIdleConns(2) db.SetConnMaxLifetime(5 * time.Minute) db.SetConnMaxIdleTime(1 * time.Minute) return db, nil } func TestFirebirdToolEndpoints(t *testing.T) { sourceConfig := getFirebirdVars(t) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() var args []string db, err := initFirebirdConnection(FirebirdHost, FirebirdPort, FirebirdUser, FirebirdPass, FirebirdDatabase) if err != nil { t.Fatalf("unable to create firebird connection pool: %s", err) } defer db.Close() shortUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8] tableNameParam := fmt.Sprintf("param_table_%s", shortUUID) tableNameAuth := fmt.Sprintf("auth_table_%s", shortUUID) tableNameTemplateParam := fmt.Sprintf("template_param_table_%s", shortUUID) createParamTableStmts, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := getFirebirdParamToolInfo(tableNameParam) teardownTable1 := setupFirebirdTable(t, ctx, db, createParamTableStmts, insertParamTableStmt, tableNameParam, paramTestParams) defer teardownTable1(t) createAuthTableStmts, insertAuthTableStmt, authToolStmt, authTestParams := getFirebirdAuthToolInfo(tableNameAuth) teardownTable2 := setupFirebirdTable(t, ctx, db, createAuthTableStmts, insertAuthTableStmt, tableNameAuth, authTestParams) defer teardownTable2(t) toolsFile := getFirebirdToolsConfig(sourceConfig, FirebirdToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) toolsFile = addFirebirdExecuteSqlConfig(t, toolsFile) tmplSelectCombined, tmplSelectFilterCombined := getFirebirdTmplToolStatement() toolsFile = addFirebirdTemplateParamConfig(t, toolsFile, FirebirdToolKind, tmplSelectCombined, tmplSelectFilterCombined) cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) } defer cleanup() waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second) defer cancelWait() 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) } // Get configs for tests select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := getFirebirdWants() nullWant := `[{"id":4,"name":null}]` select1Statement := `"SELECT 1 AS \"constant\" FROM RDB$DATABASE;"` templateParamCreateColArray := `["id INTEGER","name VARCHAR(255)","age INTEGER"]` // Run tests tests.RunToolGetTest(t) tests.RunToolInvokeTest(t, select1Want, tests.WithNullWant(nullWant), tests.DisableArrayTest()) tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want, tests.WithSelect1Statement(select1Statement)) tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.WithCreateColArray(templateParamCreateColArray)) } func setupFirebirdTable(t *testing.T, ctx context.Context, db *sql.DB, createStatements []string, insertStatement, tableName string, params []any) func(*testing.T) { err := db.PingContext(ctx) if err != nil { t.Fatalf("unable to connect to test database: %s", err) } for _, stmt := range createStatements { _, err = db.ExecContext(ctx, stmt) if err != nil { t.Fatalf("unable to execute create statement for table %s: %s\nStatement: %s", tableName, err, stmt) } } if insertStatement != "" && len(params) > 0 { stmt, err := db.PrepareContext(ctx, insertStatement) if err != nil { t.Fatalf("unable to prepare insert statement: %v", err) } defer stmt.Close() numPlaceholders := strings.Count(insertStatement, "?") if numPlaceholders == 0 { t.Fatalf("insert statement has no placeholders '?' but params were provided") } for i := 0; i < len(params); i += numPlaceholders { end := i + numPlaceholders if end > len(params) { end = len(params) } batchParams := params[i:end] _, err = stmt.ExecContext(ctx, batchParams...) if err != nil { t.Fatalf("unable to insert test data row with params %v: %v", batchParams, err) } } } return func(t *testing.T) { // Close the main connection to free up resources db.Close() // Helper function to check if error indicates object doesn't exist isNotFoundError := func(err error) bool { if err == nil { return false } errMsg := strings.ToLower(err.Error()) return strings.Contains(errMsg, "does not exist") || strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "is not defined") || strings.Contains(errMsg, "unknown") || strings.Contains(errMsg, "invalid") } // Create dedicated cleanup connection with minimal configuration createCleanupConnection := func() (*sql.DB, error) { dsn := fmt.Sprintf("%s:%s@%s:%s/%s", FirebirdUser, FirebirdPass, FirebirdHost, FirebirdPort, FirebirdDatabase) cleanupDb, err := sql.Open("firebirdsql", dsn) if err != nil { return nil, err } // Ultra minimal connection pool for cleanup only cleanupDb.SetMaxOpenConns(1) cleanupDb.SetMaxIdleConns(0) cleanupDb.SetConnMaxLifetime(5 * time.Second) cleanupDb.SetConnMaxIdleTime(1 * time.Second) return cleanupDb, nil } // Drop each object with its own dedicated connection and aggressive timeout dropObjects := []struct { objType string query string }{ {"trigger", fmt.Sprintf("DROP TRIGGER BI_%s_ID", tableName)}, {"table", fmt.Sprintf("DROP TABLE %s", tableName)}, {"generator", fmt.Sprintf("DROP GENERATOR GEN_%s_ID", tableName)}, } for _, obj := range dropObjects { cleanupDb, err := createCleanupConnection() if err != nil { t.Logf("Failed to create cleanup connection for %s: %s", obj.objType, err) continue } // Use aggressive short timeout for each operation ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) _, dropErr := cleanupDb.ExecContext(ctx, obj.query) cancel() cleanupDb.Close() if dropErr == nil { t.Logf("Successfully dropped %s", obj.objType) } else if isNotFoundError(dropErr) { t.Logf("%s does not exist, skipping", obj.objType) } else if ctx.Err() == context.DeadlineExceeded { t.Logf("Timeout dropping %s (3s limit exceeded) - continuing anyway", obj.objType) } else { t.Logf("Failed to drop %s: %s - continuing anyway", obj.objType, dropErr) } // Small delay between operations to reduce contention time.Sleep(100 * time.Millisecond) } } } func getFirebirdParamToolInfo(tableName string) ([]string, string, string, string, string, string, []any) { createStatements := []string{ fmt.Sprintf("CREATE TABLE %s (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255));", tableName), fmt.Sprintf("CREATE GENERATOR GEN_%s_ID;", tableName), fmt.Sprintf(` CREATE TRIGGER BI_%s_ID FOR %s ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL) THEN NEW.id = GEN_ID(GEN_%s_ID, 1); END; `, tableName, tableName, tableName), } insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?);", tableName) toolStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE id = ? OR name = ?;", tableName) idParamStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE id = ?;", tableName) nameParamStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE name IS NOT DISTINCT FROM ?;", tableName) // Firebird doesn't support array parameters in IN clause the same way as other databases // We'll use a simpler approach for testing arrayToolStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE id = ? ORDER BY id;", tableName) params := []any{"Alice", "Jane", "Sid", nil} return createStatements, insertStatement, toolStatement, idParamStatement, nameParamStatement, arrayToolStatement, params } func getFirebirdAuthToolInfo(tableName string) ([]string, string, string, []any) { createStatements := []string{ fmt.Sprintf("CREATE TABLE %s (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255), email VARCHAR(255));", tableName), fmt.Sprintf("CREATE GENERATOR GEN_%s_ID;", tableName), fmt.Sprintf(` CREATE TRIGGER BI_%s_ID FOR %s ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL) THEN NEW.id = GEN_ID(GEN_%s_ID, 1); END; `, tableName, tableName, tableName), } insertStatement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (?, ?)", tableName) toolStatement := fmt.Sprintf("SELECT name AS \"name\" FROM %s WHERE email = ?;", tableName) params := []any{"Alice", tests.ServiceAccountEmail, "Jane", "janedoe@gmail.com"} return createStatements, insertStatement, toolStatement, params } func getFirebirdWants() (string, string, string, string) { select1Want := `[{"constant":1}]` mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Dynamic SQL Error\nSQL error code = -104\nToken unknown - line 1, column 1\nSELEC\n"}],"isError":true}}` createTableStatement := `"CREATE TABLE t (id INTEGER PRIMARY KEY, name VARCHAR(50))"` mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"constant\":1}"}]}}` return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want } func getFirebirdToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, idParamToolStmt, nameParamToolStmt, arrayToolStatement, authToolStatement string) map[string]any { toolsFile := tests.GetToolsConfig(sourceConfig, toolKind, paramToolStatement, idParamToolStmt, nameParamToolStmt, arrayToolStatement, authToolStatement) toolsMap, ok := toolsFile["tools"].(map[string]any) if !ok { return toolsFile } if simpleTool, ok := toolsMap["my-simple-tool"].(map[string]any); ok { simpleTool["statement"] = "SELECT 1 AS \"constant\" FROM RDB$DATABASE;" toolsMap["my-simple-tool"] = simpleTool } if authRequiredTool, ok := toolsMap["my-auth-required-tool"].(map[string]any); ok { authRequiredTool["statement"] = "SELECT 1 AS \"constant\" FROM RDB$DATABASE;" toolsMap["my-auth-required-tool"] = authRequiredTool } if arrayTool, ok := toolsMap["my-array-tool"].(map[string]any); ok { // Firebird array tool - accept array but use only first element for compatibility arrayTool["parameters"] = []any{ map[string]any{ "name": "idArray", "type": "array", "description": "ID array (Firebird will use first element only)", "items": map[string]any{ "name": "id", "type": "integer", "description": "ID", }, }, } // Statement is already defined in arrayToolStatement parameter toolsMap["my-array-tool"] = arrayTool } toolsFile["tools"] = toolsMap return toolsFile } func addFirebirdTemplateParamConfig(t *testing.T, config map[string]any, toolKind, tmplSelectCombined, tmplSelectFilterCombined string) map[string]any { toolsMap, ok := config["tools"].(map[string]any) if !ok { t.Fatalf("unable to get tools from config") } // Firebird-specific template parameter tools with compatible syntax toolsMap["create-table-templateParams-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Create table tool with template parameters", "statement": "CREATE TABLE {{.tableName}} ({{array .columns}})", "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), tools.NewArrayParameter("columns", "The columns to create", tools.NewStringParameter("column", "A column name that will be created")), }, } toolsMap["insert-table-templateParams-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Insert table tool with template parameters", "statement": "INSERT INTO {{.tableName}} ({{array .columns}}) VALUES ({{.values}})", "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), tools.NewArrayParameter("columns", "The columns to insert into", tools.NewStringParameter("column", "A column name that will be returned from the query.")), tools.NewStringParameter("values", "The values to insert as a comma separated string"), }, } toolsMap["select-templateParams-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Select table tool with template parameters", "statement": "SELECT id AS \"id\", name AS \"name\", age AS \"age\" FROM {{.tableName}}", "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), }, } toolsMap["select-templateParams-combined-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Select table tool with combined template parameters", "statement": tmplSelectCombined, "parameters": []tools.Parameter{ tools.NewIntParameter("id", "the id of the user"), }, "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), }, } toolsMap["select-fields-templateParams-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Select specific fields tool with template parameters", "statement": "SELECT name AS \"name\" FROM {{.tableName}}", "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), }, } toolsMap["select-filter-templateParams-combined-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Select table tool with filter template parameters", "statement": tmplSelectFilterCombined, "parameters": []tools.Parameter{ tools.NewStringParameter("name", "the name to filter by"), }, "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), tools.NewStringParameter("columnFilter", "some description"), }, } // Firebird uses simple DROP TABLE syntax without IF EXISTS toolsMap["drop-table-templateParams-tool"] = map[string]any{ "kind": toolKind, "source": "my-instance", "description": "Drop table tool with template parameters", "statement": "DROP TABLE {{.tableName}}", "templateParameters": []tools.Parameter{ tools.NewStringParameter("tableName", "some description"), }, } config["tools"] = toolsMap return config } func addFirebirdExecuteSqlConfig(t *testing.T, config map[string]any) map[string]any { tools, ok := config["tools"].(map[string]any) if !ok { t.Fatalf("unable to get tools from config") } tools["my-exec-sql-tool"] = map[string]any{ "kind": "firebird-execute-sql", "source": "my-instance", "description": "Tool to execute sql", } tools["my-auth-exec-sql-tool"] = map[string]any{ "kind": "firebird-execute-sql", "source": "my-instance", "description": "Tool to execute sql", "authRequired": []string{ "my-google-auth", }, } config["tools"] = tools return config } func getFirebirdTmplToolStatement() (string, string) { tmplSelectCombined := "SELECT id AS \"id\", name AS \"name\", age AS \"age\" FROM {{.tableName}} WHERE id = ?" tmplSelectFilterCombined := "SELECT id AS \"id\", name AS \"name\", age AS \"age\" FROM {{.tableName}} WHERE {{.columnFilter}} = ?" return tmplSelectCombined, tmplSelectFilterCombined }

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