This is page 21 of 48. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-create-project-file.md │ │ │ ├── looker-delete-project-file.md │ │ │ ├── looker-dev-mode.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-get-project-file.md │ │ │ ├── looker-get-project-files.md │ │ │ ├── looker-get-projects.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ ├── looker-run-look.md │ │ │ └── looker-update-project-file.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookercreateprojectfile │ │ │ │ ├── lookercreateprojectfile_test.go │ │ │ │ └── lookercreateprojectfile.go │ │ │ ├── lookerdeleteprojectfile │ │ │ │ ├── lookerdeleteprojectfile_test.go │ │ │ │ └── lookerdeleteprojectfile.go │ │ │ ├── lookerdevmode │ │ │ │ ├── lookerdevmode_test.go │ │ │ │ └── lookerdevmode.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookergetprojectfile │ │ │ │ ├── lookergetprojectfile_test.go │ │ │ │ └── lookergetprojectfile.go │ │ │ ├── lookergetprojectfiles │ │ │ │ ├── lookergetprojectfiles_test.go │ │ │ │ └── lookergetprojectfiles.go │ │ │ ├── lookergetprojects │ │ │ │ ├── lookergetprojects_test.go │ │ │ │ └── lookergetprojects.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ ├── lookerrunlook │ │ │ │ ├── lookerrunlook_test.go │ │ │ │ └── lookerrunlook.go │ │ │ └── lookerupdateprojectfile │ │ │ ├── lookerupdateprojectfile_test.go │ │ │ └── lookerupdateprojectfile.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /tests/cloudsqlmysql/cloud_sql_mysql_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 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 cloudsqlmysql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "os" 22 | "regexp" 23 | "slices" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "cloud.google.com/go/cloudsqlconn" 29 | "cloud.google.com/go/cloudsqlconn/mysql/mysql" 30 | "github.com/google/uuid" 31 | "github.com/googleapis/genai-toolbox/internal/testutils" 32 | "github.com/googleapis/genai-toolbox/tests" 33 | ) 34 | 35 | var ( 36 | CloudSQLMySQLSourceKind = "cloud-sql-mysql" 37 | CloudSQLMySQLToolKind = "mysql-sql" 38 | CloudSQLMySQLProject = os.Getenv("CLOUD_SQL_MYSQL_PROJECT") 39 | CloudSQLMySQLRegion = os.Getenv("CLOUD_SQL_MYSQL_REGION") 40 | CloudSQLMySQLInstance = os.Getenv("CLOUD_SQL_MYSQL_INSTANCE") 41 | CloudSQLMySQLDatabase = os.Getenv("CLOUD_SQL_MYSQL_DATABASE") 42 | CloudSQLMySQLUser = os.Getenv("CLOUD_SQL_MYSQL_USER") 43 | CloudSQLMySQLPass = os.Getenv("CLOUD_SQL_MYSQL_PASS") 44 | ) 45 | 46 | func getCloudSQLMySQLVars(t *testing.T) map[string]any { 47 | switch "" { 48 | case CloudSQLMySQLProject: 49 | t.Fatal("'CLOUD_SQL_MYSQL_PROJECT' not set") 50 | case CloudSQLMySQLRegion: 51 | t.Fatal("'CLOUD_SQL_MYSQL_REGION' not set") 52 | case CloudSQLMySQLInstance: 53 | t.Fatal("'CLOUD_SQL_MYSQL_INSTANCE' not set") 54 | case CloudSQLMySQLDatabase: 55 | t.Fatal("'CLOUD_SQL_MYSQL_DATABASE' not set") 56 | case CloudSQLMySQLUser: 57 | t.Fatal("'CLOUD_SQL_MYSQL_USER' not set") 58 | case CloudSQLMySQLPass: 59 | t.Fatal("'CLOUD_SQL_MYSQL_PASS' not set") 60 | } 61 | 62 | return map[string]any{ 63 | "kind": CloudSQLMySQLSourceKind, 64 | "project": CloudSQLMySQLProject, 65 | "instance": CloudSQLMySQLInstance, 66 | "region": CloudSQLMySQLRegion, 67 | "database": CloudSQLMySQLDatabase, 68 | "user": CloudSQLMySQLUser, 69 | "password": CloudSQLMySQLPass, 70 | } 71 | } 72 | 73 | // Copied over from cloud_sql_mysql.go 74 | func initCloudSQLMySQLConnectionPool(project, region, instance, ipType, user, pass, dbname string) (*sql.DB, error) { 75 | 76 | // Create a new dialer with options 77 | dialOpts, err := tests.GetCloudSQLDialOpts(ipType) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if !slices.Contains(sql.Drivers(), "cloudsql-mysql") { 83 | _, err = mysql.RegisterDriver("cloudsql-mysql", cloudsqlconn.WithDefaultDialOptions(dialOpts...)) 84 | if err != nil { 85 | return nil, fmt.Errorf("unable to register driver: %w", err) 86 | } 87 | } 88 | 89 | // Tell the driver to use the Cloud SQL Go Connector to create connections 90 | dsn := fmt.Sprintf("%s:%s@cloudsql-mysql(%s:%s:%s)/%s", user, pass, project, region, instance, dbname) 91 | db, err := sql.Open( 92 | "cloudsql-mysql", 93 | dsn, 94 | ) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return db, nil 99 | } 100 | 101 | func TestCloudSQLMySQLToolEndpoints(t *testing.T) { 102 | sourceConfig := getCloudSQLMySQLVars(t) 103 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 104 | defer cancel() 105 | 106 | var args []string 107 | 108 | pool, err := initCloudSQLMySQLConnectionPool(CloudSQLMySQLProject, CloudSQLMySQLRegion, CloudSQLMySQLInstance, "public", CloudSQLMySQLUser, CloudSQLMySQLPass, CloudSQLMySQLDatabase) 109 | if err != nil { 110 | t.Fatalf("unable to create Cloud SQL connection pool: %s", err) 111 | } 112 | 113 | // cleanup test environment 114 | tests.CleanupMySQLTables(t, ctx, pool) 115 | 116 | // create table name with UUID 117 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 118 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 119 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 120 | 121 | // set up data for param tool 122 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetMySQLParamToolInfo(tableNameParam) 123 | teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 124 | defer teardownTable1(t) 125 | 126 | // set up data for auth tool 127 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMySQLAuthToolInfo(tableNameAuth) 128 | teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 129 | defer teardownTable2(t) 130 | 131 | // Write config into a file and pass it to command 132 | toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMySQLToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 133 | toolsFile = tests.AddMySqlExecuteSqlConfig(t, toolsFile) 134 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement() 135 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLMySQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") 136 | toolsFile = tests.AddMySQLPrebuiltToolConfig(t, toolsFile) 137 | 138 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 139 | if err != nil { 140 | t.Fatalf("command initialization returned an error: %s", err) 141 | } 142 | defer cleanup() 143 | 144 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 145 | defer cancel() 146 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 147 | if err != nil { 148 | t.Logf("toolbox command logs: \n%s", out) 149 | t.Fatalf("toolbox didn't start successfully: %s", err) 150 | } 151 | 152 | // Get configs for tests 153 | select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMySQLWants() 154 | 155 | // Run tests 156 | tests.RunToolGetTest(t) 157 | tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest()) 158 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 159 | tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) 160 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 161 | 162 | // Run specific MySQL tool tests 163 | tests.RunMySQLListTablesTest(t, CloudSQLMySQLDatabase, tableNameParam, tableNameAuth) 164 | tests.RunMySQLListActiveQueriesTest(t, ctx, pool) 165 | } 166 | 167 | // Test connection with different IP type 168 | func TestCloudSQLMySQLIpConnection(t *testing.T) { 169 | sourceConfig := getCloudSQLMySQLVars(t) 170 | 171 | tcs := []struct { 172 | name string 173 | ipType string 174 | }{ 175 | { 176 | name: "public ip", 177 | ipType: "public", 178 | }, 179 | { 180 | name: "private ip", 181 | ipType: "private", 182 | }, 183 | } 184 | for _, tc := range tcs { 185 | t.Run(tc.name, func(t *testing.T) { 186 | sourceConfig["ipType"] = tc.ipType 187 | err := tests.RunSourceConnectionTest(t, sourceConfig, CloudSQLMySQLToolKind) 188 | if err != nil { 189 | t.Fatalf("Connection test failure: %s", err) 190 | } 191 | }) 192 | } 193 | } 194 | ``` -------------------------------------------------------------------------------- /internal/tools/bigtable/bigtable.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 bigtable 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "cloud.google.com/go/bigtable" 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | bigtabledb "github.com/googleapis/genai-toolbox/internal/sources/bigtable" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | ) 27 | 28 | const kind string = "bigtable-sql" 29 | 30 | func init() { 31 | if !tools.Register(kind, newConfig) { 32 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 33 | } 34 | } 35 | 36 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 37 | actual := Config{Name: name} 38 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 39 | return nil, err 40 | } 41 | return actual, nil 42 | } 43 | 44 | type compatibleSource interface { 45 | BigtableClient() *bigtable.Client 46 | } 47 | 48 | // validate compatible sources are still compatible 49 | var _ compatibleSource = &bigtabledb.Source{} 50 | 51 | var compatibleSources = [...]string{bigtabledb.SourceKind} 52 | 53 | type Config struct { 54 | Name string `yaml:"name" validate:"required"` 55 | Kind string `yaml:"kind" validate:"required"` 56 | Source string `yaml:"source" validate:"required"` 57 | Description string `yaml:"description" validate:"required"` 58 | Statement string `yaml:"statement" validate:"required"` 59 | AuthRequired []string `yaml:"authRequired"` 60 | Parameters tools.Parameters `yaml:"parameters"` 61 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 62 | } 63 | 64 | // validate interface 65 | var _ tools.ToolConfig = Config{} 66 | 67 | func (cfg Config) ToolConfigKind() string { 68 | return kind 69 | } 70 | 71 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 72 | // verify source exists 73 | rawS, ok := srcs[cfg.Source] 74 | if !ok { 75 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 76 | } 77 | 78 | // verify the source is compatible 79 | s, ok := rawS.(compatibleSource) 80 | if !ok { 81 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 82 | } 83 | 84 | allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 90 | 91 | // finish tool setup 92 | t := Tool{ 93 | Name: cfg.Name, 94 | Kind: kind, 95 | Parameters: cfg.Parameters, 96 | TemplateParameters: cfg.TemplateParameters, 97 | AllParams: allParameters, 98 | Statement: cfg.Statement, 99 | AuthRequired: cfg.AuthRequired, 100 | Client: s.BigtableClient(), 101 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 102 | mcpManifest: mcpManifest, 103 | } 104 | return t, nil 105 | } 106 | 107 | // validate interface 108 | var _ tools.Tool = Tool{} 109 | 110 | type Tool struct { 111 | Name string `yaml:"name"` 112 | Kind string `yaml:"kind"` 113 | AuthRequired []string `yaml:"authRequired"` 114 | Parameters tools.Parameters `yaml:"parameters"` 115 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 116 | AllParams tools.Parameters `yaml:"allParams"` 117 | 118 | Client *bigtable.Client 119 | Statement string 120 | manifest tools.Manifest 121 | mcpManifest tools.McpManifest 122 | } 123 | 124 | func getBigtableType(paramType string) (bigtable.SQLType, error) { 125 | switch paramType { 126 | case "boolean": 127 | return bigtable.BoolSQLType{}, nil 128 | case "string": 129 | return bigtable.StringSQLType{}, nil 130 | case "integer": 131 | return bigtable.Int64SQLType{}, nil 132 | case "float": 133 | return bigtable.Float64SQLType{}, nil 134 | case "array": 135 | return bigtable.ArraySQLType{}, nil 136 | default: 137 | return nil, fmt.Errorf("unknow param type %s", paramType) 138 | } 139 | } 140 | 141 | func getMapParamsType(tparams tools.Parameters, params tools.ParamValues) (map[string]bigtable.SQLType, error) { 142 | btParamTypes := make(map[string]bigtable.SQLType) 143 | for _, p := range tparams { 144 | if p.GetType() == "array" { 145 | itemType, err := getBigtableType(p.Manifest().Items.Type) 146 | if err != nil { 147 | return nil, err 148 | } 149 | btParamTypes[p.GetName()] = bigtable.ArraySQLType{ 150 | ElemType: itemType, 151 | } 152 | continue 153 | } 154 | paramType, err := getBigtableType(p.GetType()) 155 | if err != nil { 156 | return nil, err 157 | } 158 | btParamTypes[p.GetName()] = paramType 159 | } 160 | return btParamTypes, nil 161 | } 162 | 163 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 164 | paramsMap := params.AsMap() 165 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 166 | if err != nil { 167 | return nil, fmt.Errorf("unable to extract template params %w", err) 168 | } 169 | 170 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 171 | if err != nil { 172 | return nil, fmt.Errorf("unable to extract standard params %w", err) 173 | } 174 | 175 | mapParamsType, err := getMapParamsType(t.Parameters, newParams) 176 | if err != nil { 177 | return nil, fmt.Errorf("fail to get map params: %w", err) 178 | } 179 | 180 | ps, err := t.Client.PrepareStatement( 181 | ctx, 182 | newStatement, 183 | mapParamsType, 184 | ) 185 | if err != nil { 186 | return nil, fmt.Errorf("unable to prepare statement: %w", err) 187 | } 188 | 189 | bs, err := ps.Bind(newParams.AsMap()) 190 | if err != nil { 191 | return nil, fmt.Errorf("unable to bind: %w", err) 192 | } 193 | 194 | var out []any 195 | err = bs.Execute(ctx, func(resultRow bigtable.ResultRow) bool { 196 | vMap := make(map[string]any) 197 | cols := resultRow.Metadata.Columns 198 | 199 | for _, c := range cols { 200 | var columValue any 201 | err = resultRow.GetByName(c.Name, &columValue) 202 | vMap[c.Name] = columValue 203 | } 204 | 205 | out = append(out, vMap) 206 | 207 | return true 208 | }) 209 | if err != nil { 210 | return nil, fmt.Errorf("unable to execute client: %w", err) 211 | } 212 | 213 | return out, nil 214 | } 215 | 216 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 217 | return tools.ParseParams(t.AllParams, data, claims) 218 | } 219 | 220 | func (t Tool) Manifest() tools.Manifest { 221 | return t.manifest 222 | } 223 | 224 | func (t Tool) McpManifest() tools.McpManifest { 225 | return t.mcpManifest 226 | } 227 | 228 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 229 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 230 | } 231 | 232 | func (t Tool) RequiresClientAuthorization() bool { 233 | return false 234 | } 235 | ``` -------------------------------------------------------------------------------- /internal/tools/oracle/oraclesql/oraclesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright © 2025, Oracle and/or its affiliates. 2 | 3 | package oraclesql 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | "encoding/json" 9 | "fmt" 10 | "strings" 11 | 12 | yaml "github.com/goccy/go-yaml" 13 | "github.com/googleapis/genai-toolbox/internal/sources" 14 | "github.com/googleapis/genai-toolbox/internal/sources/oracle" 15 | "github.com/googleapis/genai-toolbox/internal/tools" 16 | ) 17 | 18 | const kind string = "oracle-sql" 19 | 20 | func init() { 21 | if !tools.Register(kind, newConfig) { 22 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 23 | } 24 | } 25 | 26 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 27 | actual := Config{Name: name} 28 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 29 | return nil, err 30 | } 31 | return actual, nil 32 | } 33 | 34 | type compatibleSource interface { 35 | OracleDB() *sql.DB 36 | } 37 | 38 | // validate compatible sources are still compatible 39 | var _ compatibleSource = &oracle.Source{} 40 | 41 | var compatibleSources = [...]string{oracle.SourceKind} 42 | 43 | type Config struct { 44 | Name string `yaml:"name" validate:"required"` 45 | Kind string `yaml:"kind" validate:"required"` 46 | Source string `yaml:"source" validate:"required"` 47 | Description string `yaml:"description" validate:"required"` 48 | Statement string `yaml:"statement" validate:"required"` 49 | AuthRequired []string `yaml:"authRequired"` 50 | Parameters tools.Parameters `yaml:"parameters"` 51 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 52 | } 53 | 54 | // validate interface 55 | var _ tools.ToolConfig = Config{} 56 | 57 | func (cfg Config) ToolConfigKind() string { 58 | return kind 59 | } 60 | 61 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 62 | // verify source exists 63 | rawS, ok := srcs[cfg.Source] 64 | if !ok { 65 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 66 | } 67 | 68 | // verify the source is compatible 69 | s, ok := rawS.(compatibleSource) 70 | if !ok { 71 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 72 | } 73 | 74 | allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 75 | if err != nil { 76 | return nil, fmt.Errorf("error processing parameters: %w", err) 77 | } 78 | 79 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 80 | 81 | // finish tool setup 82 | t := Tool{ 83 | Name: cfg.Name, 84 | Kind: kind, 85 | Parameters: cfg.Parameters, 86 | TemplateParameters: cfg.TemplateParameters, 87 | AllParams: allParameters, 88 | Statement: cfg.Statement, 89 | AuthRequired: cfg.AuthRequired, 90 | DB: s.OracleDB(), 91 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 92 | mcpManifest: mcpManifest, 93 | } 94 | return t, nil 95 | } 96 | 97 | // validate interface 98 | var _ tools.Tool = Tool{} 99 | 100 | type Tool struct { 101 | Name string `yaml:"name"` 102 | Kind string `yaml:"kind"` 103 | AuthRequired []string `yaml:"authRequired"` 104 | Parameters tools.Parameters `yaml:"parameters"` 105 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 106 | AllParams tools.Parameters `yaml:"allParams"` 107 | 108 | DB *sql.DB 109 | Statement string 110 | manifest tools.Manifest 111 | mcpManifest tools.McpManifest 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | paramsMap := params.AsMap() 116 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 117 | if err != nil { 118 | return nil, fmt.Errorf("unable to extract template params %w", err) 119 | } 120 | 121 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 122 | if err != nil { 123 | return nil, fmt.Errorf("unable to extract standard params %w", err) 124 | } 125 | sliceParams := newParams.AsSlice() 126 | 127 | for i, p := range sliceParams { 128 | fmt.Printf("[%d]=%T ", i, p) 129 | } 130 | fmt.Printf("\n") 131 | 132 | rows, err := t.DB.QueryContext(ctx, newStatement, sliceParams...) 133 | if err != nil { 134 | return nil, fmt.Errorf("unable to execute query: %w", err) 135 | } 136 | defer rows.Close() 137 | 138 | cols, _ := rows.Columns() 139 | 140 | // Get Column types 141 | colTypes, err := rows.ColumnTypes() 142 | if err != nil { 143 | return nil, fmt.Errorf("unable to get column types: %w", err) 144 | } 145 | 146 | var out []any 147 | for rows.Next() { 148 | values := make([]any, len(cols)) 149 | for i, colType := range colTypes { 150 | switch strings.ToUpper(colType.DatabaseTypeName()) { 151 | case "NUMBER", "FLOAT", "BINARY_FLOAT", "BINARY_DOUBLE": 152 | if _, scale, ok := colType.DecimalSize(); ok && scale == 0 { 153 | // Scale is 0, treat it as an integer. 154 | values[i] = new(sql.NullInt64) 155 | } else { 156 | // Scale is non-zero or unknown, treat 157 | // it as a float. 158 | values[i] = new(sql.NullFloat64) 159 | } 160 | case "DATE", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE": 161 | values[i] = new(sql.NullTime) 162 | case "JSON": 163 | values[i] = new(sql.RawBytes) 164 | default: 165 | values[i] = new(sql.NullString) 166 | } 167 | } 168 | 169 | if err := rows.Scan(values...); err != nil { 170 | return nil, fmt.Errorf("unable to scan row: %w", err) 171 | } 172 | 173 | vMap := make(map[string]any) 174 | for i, col := range cols { 175 | receiver := values[i] 176 | 177 | switch v := receiver.(type) { 178 | case *sql.NullInt64: 179 | if v.Valid { 180 | vMap[col] = v.Int64 181 | } else { 182 | vMap[col] = nil 183 | } 184 | case *sql.NullFloat64: 185 | if v.Valid { 186 | vMap[col] = v.Float64 187 | } else { 188 | vMap[col] = nil 189 | } 190 | case *sql.NullString: 191 | if v.Valid { 192 | vMap[col] = v.String 193 | } else { 194 | vMap[col] = nil 195 | } 196 | case *sql.NullTime: 197 | if v.Valid { 198 | vMap[col] = v.Time 199 | } else { 200 | vMap[col] = nil 201 | } 202 | case *sql.RawBytes: 203 | if *v != nil { 204 | var unmarshaledData any 205 | if err := json.Unmarshal(*v, &unmarshaledData); err != nil { 206 | return nil, fmt.Errorf("unable to unmarshal json data for column %s", col) 207 | } 208 | vMap[col] = unmarshaledData 209 | } else { 210 | vMap[col] = nil 211 | } 212 | default: 213 | return nil, fmt.Errorf("unexpected receiver type: %T", v) 214 | } 215 | } 216 | out = append(out, vMap) 217 | } 218 | 219 | if err := rows.Err(); err != nil { 220 | return nil, fmt.Errorf("errors encountered during query execution or row processing: %w", err) 221 | } 222 | 223 | return out, nil 224 | } 225 | 226 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 227 | return tools.ParseParams(t.AllParams, data, claims) 228 | } 229 | 230 | func (t Tool) Manifest() tools.Manifest { 231 | return t.manifest 232 | } 233 | 234 | func (t Tool) McpManifest() tools.McpManifest { 235 | return t.mcpManifest 236 | } 237 | 238 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 239 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 240 | } 241 | 242 | func (t Tool) RequiresClientAuthorization() bool { 243 | return false 244 | } 245 | ``` -------------------------------------------------------------------------------- /docs/en/resources/sources/alloydb-pg.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "AlloyDB for PostgreSQL" 3 | linkTitle: "AlloyDB" 4 | type: docs 5 | weight: 1 6 | description: > 7 | AlloyDB for PostgreSQL is a fully-managed, PostgreSQL-compatible database for 8 | demanding transactional workloads. 9 | 10 | --- 11 | 12 | ## About 13 | 14 | [AlloyDB for PostgreSQL][alloydb-docs] is a fully-managed, PostgreSQL-compatible 15 | database for demanding transactional workloads. It provides enterprise-grade 16 | performance and availability while maintaining 100% compatibility with 17 | open-source PostgreSQL. 18 | 19 | If you are new to AlloyDB for PostgreSQL, you can [create a free trial 20 | cluster][alloydb-free-trial]. 21 | 22 | [alloydb-docs]: https://cloud.google.com/alloydb/docs 23 | [alloydb-free-trial]: https://cloud.google.com/alloydb/docs/create-free-trial-cluster 24 | 25 | ## Available Tools 26 | 27 | - [`alloydb-ai-nl`](../tools/alloydbainl/alloydb-ai-nl.md) 28 | Use natural language queries on AlloyDB, powered by AlloyDB AI. 29 | 30 | - [`postgres-sql`](../tools/postgres/postgres-sql.md) 31 | Execute SQL queries as prepared statements in AlloyDB Postgres. 32 | 33 | - [`postgres-execute-sql`](../tools/postgres/postgres-execute-sql.md) 34 | Run parameterized SQL statements in AlloyDB Postgres. 35 | 36 | - [`postgres-list-tables`](../tools/postgres/postgres-list-tables.md) 37 | List tables in an AlloyDB for PostgreSQL database. 38 | 39 | - [`postgres-list-active-queries`](../tools/postgres/postgres-list-active-queries.md) 40 | List active queries in an AlloyDB for PostgreSQL database. 41 | 42 | - [`postgres-list-available-extensions`](../tools/postgres/postgres-list-available-extensions.md) 43 | List available extensions for installation in a PostgreSQL database. 44 | 45 | - [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md) 46 | List installed extensions in a PostgreSQL database. 47 | 48 | ### Pre-built Configurations 49 | 50 | - [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/) 51 | Connect your IDE to AlloyDB using Toolbox. 52 | 53 | - [AlloyDB Admin API using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_admin_mcp/) 54 | Create your AlloyDB database with MCP Toolbox. 55 | 56 | ## Requirements 57 | 58 | ### IAM Permissions 59 | 60 | By default, AlloyDB for PostgreSQL source uses the [AlloyDB Go 61 | Connector][alloydb-go-conn] to authorize and establish mTLS connections to your 62 | AlloyDB instance. The Go connector uses your [Application Default Credentials 63 | (ADC)][adc] to authorize your connection to AlloyDB. 64 | 65 | In addition to [setting the ADC for your server][set-adc], you need to ensure 66 | the IAM identity has been given the following IAM roles (or corresponding 67 | permissions): 68 | 69 | - `roles/alloydb.client` 70 | - `roles/serviceusage.serviceUsageConsumer` 71 | 72 | [alloydb-go-conn]: https://github.com/GoogleCloudPlatform/alloydb-go-connector 73 | [adc]: https://cloud.google.com/docs/authentication#adc 74 | [set-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc 75 | 76 | ### Networking 77 | 78 | AlloyDB supports connecting over both from external networks via the internet 79 | ([public IP][public-ip]), and internal networks ([private IP][private-ip]). 80 | For more information on choosing between the two options, see the AlloyDB page 81 | [Connection overview][conn-overview]. 82 | 83 | You can configure the `ipType` parameter in your source configuration to 84 | `public` or `private` to match your cluster's configuration. Regardless of which 85 | you choose, all connections use IAM-based authorization and are encrypted with 86 | mTLS. 87 | 88 | [private-ip]: https://cloud.google.com/alloydb/docs/private-ip 89 | [public-ip]: https://cloud.google.com/alloydb/docs/connect-public-ip 90 | [conn-overview]: https://cloud.google.com/alloydb/docs/connection-overview 91 | 92 | ### Authentication 93 | 94 | This source supports both password-based authentication and IAM 95 | authentication (using your [Application Default Credentials][adc]). 96 | 97 | #### Standard Authentication 98 | 99 | To connect using user/password, [create 100 | a PostgreSQL user][alloydb-users] and input your credentials in the `user` and 101 | `password` fields. 102 | 103 | ```yaml 104 | user: ${USER_NAME} 105 | password: ${PASSWORD} 106 | ``` 107 | 108 | #### IAM Authentication 109 | 110 | To connect using IAM authentication: 111 | 112 | 1. Prepare your database instance and user following this [guide][iam-guide]. 113 | 2. You could choose one of the two ways to log in: 114 | - Specify your IAM email as the `user`. 115 | - Leave your `user` field blank. Toolbox will fetch the [ADC][adc] 116 | automatically and log in using the email associated with it. 117 | 3. Leave the `password` field blank. 118 | 119 | [iam-guide]: https://cloud.google.com/alloydb/docs/database-users/manage-iam-auth 120 | [alloydb-users]: https://cloud.google.com/alloydb/docs/database-users/about 121 | 122 | ## Example 123 | 124 | ```yaml 125 | sources: 126 | my-alloydb-pg-source: 127 | kind: alloydb-postgres 128 | project: my-project-id 129 | region: us-central1 130 | cluster: my-cluster 131 | instance: my-instance 132 | database: my_db 133 | user: ${USER_NAME} 134 | password: ${PASSWORD} 135 | # ipType: "public" 136 | ``` 137 | 138 | {{< notice tip >}} 139 | Use environment variable replacement with the format ${ENV_NAME} 140 | instead of hardcoding your secrets into the configuration file. 141 | {{< /notice >}} 142 | 143 | ## Reference 144 | 145 | | **field** | **type** | **required** | **description** | 146 | |-----------|:--------:|:------------:|--------------------------------------------------------------------------------------------------------------------------| 147 | | kind | string | true | Must be "alloydb-postgres". | 148 | | project | string | true | Id of the GCP project that the cluster was created in (e.g. "my-project-id"). | 149 | | region | string | true | Name of the GCP region that the cluster was created in (e.g. "us-central1"). | 150 | | cluster | string | true | Name of the AlloyDB cluster (e.g. "my-cluster"). | 151 | | instance | string | true | Name of the AlloyDB instance within the cluster (e.g. "my-instance"). | 152 | | database | string | true | Name of the Postgres database to connect to (e.g. "my_db"). | 153 | | user | string | false | Name of the Postgres user to connect as (e.g. "my-pg-user"). Defaults to IAM auth using [ADC][adc] email if unspecified. | 154 | | password | string | false | Password of the Postgres user (e.g. "my-password"). Defaults to attempting IAM authentication if unspecified. | 155 | | ipType | string | false | IP Type of the AlloyDB instance; must be one of `public` or `private`. Default: `public`. | 156 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookermakelook/lookermakelook.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 | package lookermakelook 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "fmt" 20 | "slices" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" 27 | "github.com/googleapis/genai-toolbox/internal/util" 28 | 29 | "github.com/looker-open-source/sdk-codegen/go/rtl" 30 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 31 | ) 32 | 33 | const kind string = "looker-make-look" 34 | 35 | func init() { 36 | if !tools.Register(kind, newConfig) { 37 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 38 | } 39 | } 40 | 41 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 42 | actual := Config{Name: name} 43 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 44 | return nil, err 45 | } 46 | return actual, nil 47 | } 48 | 49 | type Config struct { 50 | Name string `yaml:"name" validate:"required"` 51 | Kind string `yaml:"kind" validate:"required"` 52 | Source string `yaml:"source" validate:"required"` 53 | Description string `yaml:"description" validate:"required"` 54 | AuthRequired []string `yaml:"authRequired"` 55 | } 56 | 57 | // validate interface 58 | var _ tools.ToolConfig = Config{} 59 | 60 | func (cfg Config) ToolConfigKind() string { 61 | return kind 62 | } 63 | 64 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 65 | // verify source exists 66 | rawS, ok := srcs[cfg.Source] 67 | if !ok { 68 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 69 | } 70 | 71 | // verify the source is compatible 72 | s, ok := rawS.(*lookersrc.Source) 73 | if !ok { 74 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 75 | } 76 | 77 | parameters := lookercommon.GetQueryParameters() 78 | 79 | titleParameter := tools.NewStringParameter("title", "The title of the Look") 80 | parameters = append(parameters, titleParameter) 81 | descParameter := tools.NewStringParameterWithDefault("description", "", "The description of the Look") 82 | parameters = append(parameters, descParameter) 83 | vizParameter := tools.NewMapParameterWithDefault("vis_config", 84 | map[string]any{}, 85 | "The visualization config for the query", 86 | "", 87 | ) 88 | parameters = append(parameters, vizParameter) 89 | 90 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 91 | 92 | // finish tool setup 93 | return Tool{ 94 | Name: cfg.Name, 95 | Kind: kind, 96 | Parameters: parameters, 97 | AuthRequired: cfg.AuthRequired, 98 | UseClientOAuth: s.UseClientOAuth, 99 | Client: s.Client, 100 | ApiSettings: s.ApiSettings, 101 | manifest: tools.Manifest{ 102 | Description: cfg.Description, 103 | Parameters: parameters.Manifest(), 104 | AuthRequired: cfg.AuthRequired, 105 | }, 106 | mcpManifest: mcpManifest, 107 | }, nil 108 | } 109 | 110 | // validate interface 111 | var _ tools.Tool = Tool{} 112 | 113 | type Tool struct { 114 | Name string `yaml:"name"` 115 | Kind string `yaml:"kind"` 116 | UseClientOAuth bool 117 | Client *v4.LookerSDK 118 | ApiSettings *rtl.ApiSettings 119 | AuthRequired []string `yaml:"authRequired"` 120 | Parameters tools.Parameters `yaml:"parameters"` 121 | manifest tools.Manifest 122 | mcpManifest tools.McpManifest 123 | } 124 | 125 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 126 | logger, err := util.LoggerFromContext(ctx) 127 | if err != nil { 128 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 129 | } 130 | logger.DebugContext(ctx, "params = ", params) 131 | wq, err := lookercommon.ProcessQueryArgs(ctx, params) 132 | if err != nil { 133 | return nil, fmt.Errorf("error building query request: %w", err) 134 | } 135 | 136 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 137 | if err != nil { 138 | return nil, fmt.Errorf("error getting sdk: %w", err) 139 | } 140 | mrespFields := "id,personal_folder_id" 141 | mresp, err := sdk.Me(mrespFields, t.ApiSettings) 142 | if err != nil { 143 | return nil, fmt.Errorf("error making me request: %s", err) 144 | } 145 | 146 | paramsMap := params.AsMap() 147 | title := paramsMap["title"].(string) 148 | description := paramsMap["description"].(string) 149 | 150 | looks, err := sdk.FolderLooks(*mresp.PersonalFolderId, "title", t.ApiSettings) 151 | if err != nil { 152 | return nil, fmt.Errorf("error getting existing looks in folder: %s", err) 153 | } 154 | 155 | lookTitles := []string{} 156 | for _, look := range looks { 157 | lookTitles = append(lookTitles, *look.Title) 158 | } 159 | if slices.Contains(lookTitles, title) { 160 | lt, _ := json.Marshal(lookTitles) 161 | return nil, fmt.Errorf("title %s already used in user's folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt)) 162 | } 163 | 164 | visConfig := paramsMap["vis_config"].(map[string]any) 165 | wq.VisConfig = &visConfig 166 | 167 | qrespFields := "id" 168 | qresp, err := sdk.CreateQuery(*wq, qrespFields, t.ApiSettings) 169 | if err != nil { 170 | return nil, fmt.Errorf("error making create query request: %s", err) 171 | } 172 | 173 | wlwq := v4.WriteLookWithQuery{ 174 | Title: &title, 175 | UserId: mresp.Id, 176 | Description: &description, 177 | QueryId: qresp.Id, 178 | FolderId: mresp.PersonalFolderId, 179 | } 180 | resp, err := sdk.CreateLook(wlwq, "", t.ApiSettings) 181 | if err != nil { 182 | return nil, fmt.Errorf("error making create look request: %s", err) 183 | } 184 | logger.DebugContext(ctx, "resp = %v", resp) 185 | 186 | setting, err := sdk.GetSetting("host_url", t.ApiSettings) 187 | if err != nil { 188 | logger.ErrorContext(ctx, "error getting settings: %s", err) 189 | } 190 | 191 | data := make(map[string]any) 192 | if resp.Id != nil { 193 | data["id"] = *resp.Id 194 | } 195 | if resp.ShortUrl != nil { 196 | if setting.HostUrl != nil { 197 | data["short_url"] = *setting.HostUrl + *resp.ShortUrl 198 | } else { 199 | data["short_url"] = *resp.ShortUrl 200 | } 201 | } 202 | logger.DebugContext(ctx, "data = %v", data) 203 | 204 | return data, nil 205 | } 206 | 207 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 208 | return tools.ParseParams(t.Parameters, data, claims) 209 | } 210 | 211 | func (t Tool) Manifest() tools.Manifest { 212 | return t.manifest 213 | } 214 | 215 | func (t Tool) McpManifest() tools.McpManifest { 216 | return t.mcpManifest 217 | } 218 | 219 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 220 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 221 | } 222 | 223 | func (t Tool) RequiresClientAuthorization() bool { 224 | return t.UseClientOAuth 225 | } 226 | ``` -------------------------------------------------------------------------------- /tests/cloudsql/cloud_sql_create_users_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 cloudsql 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httptest" 25 | "net/url" 26 | "reflect" 27 | "regexp" 28 | "strings" 29 | "testing" 30 | "time" 31 | 32 | "github.com/google/go-cmp/cmp" 33 | "github.com/googleapis/genai-toolbox/internal/testutils" 34 | "github.com/googleapis/genai-toolbox/tests" 35 | ) 36 | 37 | var ( 38 | createUserToolKind = "cloud-sql-create-users" 39 | ) 40 | 41 | type createUsersTransport struct { 42 | transport http.RoundTripper 43 | url *url.URL 44 | } 45 | 46 | func (t *createUsersTransport) RoundTrip(req *http.Request) (*http.Response, error) { 47 | if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { 48 | req.URL.Scheme = t.url.Scheme 49 | req.URL.Host = t.url.Host 50 | } 51 | return t.transport.RoundTrip(req) 52 | } 53 | 54 | type userCreateRequest struct { 55 | Name string `json:"name"` 56 | Password string `json:"password,omitempty"` 57 | Type string `json:"type,omitempty"` 58 | } 59 | 60 | type masterCreateUserHandler struct { 61 | t *testing.T 62 | } 63 | 64 | func (h *masterCreateUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 65 | if !strings.Contains(r.UserAgent(), "genai-toolbox/") { 66 | h.t.Errorf("User-Agent header not found") 67 | } 68 | 69 | var body userCreateRequest 70 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 71 | h.t.Fatalf("failed to decode request body: %v", err) 72 | } 73 | 74 | var expectedBody userCreateRequest 75 | var response any 76 | var statusCode int 77 | 78 | switch body.Name { 79 | case "test-user": 80 | expectedBody = userCreateRequest{Name: "test-user", Password: "password", Type: "BUILT_IN"} 81 | response = map[string]any{"name": "op1", "status": "PENDING"} 82 | statusCode = http.StatusOK 83 | case "iam-user": 84 | expectedBody = userCreateRequest{Name: "iam-user", Type: "CLOUD_IAM_USER"} 85 | response = map[string]any{"name": "op2", "status": "PENDING"} 86 | statusCode = http.StatusOK 87 | default: 88 | http.Error(w, fmt.Sprintf("unhandled user name: %s", body.Name), http.StatusInternalServerError) 89 | return 90 | } 91 | 92 | // For IAM user, password is not expected 93 | if body.Type == "CLOUD_IAM_USER" { 94 | expectedBody.Password = "" 95 | } 96 | 97 | if diff := cmp.Diff(expectedBody, body); diff != "" { 98 | h.t.Errorf("unexpected request body (-want +got):\n%s", diff) 99 | } 100 | 101 | w.Header().Set("Content-Type", "application/json") 102 | w.WriteHeader(statusCode) 103 | if err := json.NewEncoder(w).Encode(response); err != nil { 104 | http.Error(w, err.Error(), http.StatusInternalServerError) 105 | } 106 | } 107 | 108 | func TestCreateUsersToolEndpoints(t *testing.T) { 109 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 110 | defer cancel() 111 | 112 | handler := &masterCreateUserHandler{t: t} 113 | server := httptest.NewServer(handler) 114 | defer server.Close() 115 | 116 | serverURL, err := url.Parse(server.URL) 117 | if err != nil { 118 | t.Fatalf("failed to parse server URL: %v", err) 119 | } 120 | 121 | originalTransport := http.DefaultClient.Transport 122 | if originalTransport == nil { 123 | originalTransport = http.DefaultTransport 124 | } 125 | http.DefaultClient.Transport = &createUsersTransport{ 126 | transport: originalTransport, 127 | url: serverURL, 128 | } 129 | t.Cleanup(func() { 130 | http.DefaultClient.Transport = originalTransport 131 | }) 132 | 133 | var args []string 134 | toolsFile := getCreateUsersToolsConfig() 135 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 136 | if err != nil { 137 | t.Fatalf("command initialization returned an error: %s", err) 138 | } 139 | defer cleanup() 140 | 141 | waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) 142 | defer cancel() 143 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 144 | if err != nil { 145 | t.Logf("toolbox command logs: \n%s", out) 146 | t.Fatalf("toolbox didn't start successfully: %s", err) 147 | } 148 | 149 | tcs := []struct { 150 | name string 151 | toolName string 152 | body string 153 | want string 154 | expectError bool 155 | errorStatus int 156 | }{ 157 | { 158 | name: "successful built-in user creation", 159 | toolName: "create-user", 160 | body: `{"project": "p1", "instance": "i1", "name": "test-user", "password": "password", "iamUser": false}`, 161 | want: `{"name":"op1","status":"PENDING"}`, 162 | }, 163 | { 164 | name: "successful iam user creation", 165 | toolName: "create-user", 166 | body: `{"project": "p1", "instance": "i1", "name": "iam-user", "iamUser": true}`, 167 | want: `{"name":"op2","status":"PENDING"}`, 168 | }, 169 | { 170 | name: "missing password for built-in user", 171 | toolName: "create-user", 172 | body: `{"project": "p1", "instance": "i1", "name": "test-user", "iamUser": false}`, 173 | expectError: true, 174 | errorStatus: http.StatusBadRequest, 175 | }, 176 | } 177 | 178 | for _, tc := range tcs { 179 | tc := tc 180 | t.Run(tc.name, func(t *testing.T) { 181 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 182 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 183 | if err != nil { 184 | t.Fatalf("unable to create request: %s", err) 185 | } 186 | req.Header.Add("Content-type", "application/json") 187 | resp, err := http.DefaultClient.Do(req) 188 | if err != nil { 189 | t.Fatalf("unable to send request: %s", err) 190 | } 191 | defer resp.Body.Close() 192 | 193 | if tc.expectError { 194 | if resp.StatusCode != tc.errorStatus { 195 | bodyBytes, _ := io.ReadAll(resp.Body) 196 | t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) 197 | } 198 | return 199 | } 200 | 201 | if resp.StatusCode != http.StatusOK { 202 | bodyBytes, _ := io.ReadAll(resp.Body) 203 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 204 | } 205 | 206 | var result struct { 207 | Result string `json:"result"` 208 | } 209 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 210 | t.Fatalf("failed to decode response: %v", err) 211 | } 212 | 213 | var got, want map[string]any 214 | if err := json.Unmarshal([]byte(result.Result), &got); err != nil { 215 | t.Fatalf("failed to unmarshal result: %v", err) 216 | } 217 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 218 | t.Fatalf("failed to unmarshal want: %v", err) 219 | } 220 | 221 | if !reflect.DeepEqual(got, want) { 222 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 223 | } 224 | }) 225 | } 226 | } 227 | 228 | func getCreateUsersToolsConfig() map[string]any { 229 | return map[string]any{ 230 | "sources": map[string]any{ 231 | "my-cloud-sql-source": map[string]any{ 232 | "kind": "cloud-sql-admin", 233 | }, 234 | }, 235 | "tools": map[string]any{ 236 | "create-user": map[string]any{ 237 | "kind": createUserToolKind, 238 | "source": "my-cloud-sql-source", 239 | }, 240 | }, 241 | } 242 | } 243 | ``` -------------------------------------------------------------------------------- /internal/tools/cloudsqlmysql/cloudsqlmysqlcreateinstance/cloudsqlmysqlcreateinstance.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 cloudsqlmysqlcreateinstance 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | sqladmin "google.golang.org/api/sqladmin/v1" 27 | ) 28 | 29 | const kind string = "cloud-sql-mysql-create-instance" 30 | 31 | func init() { 32 | if !tools.Register(kind, newConfig) { 33 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 34 | } 35 | } 36 | 37 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 38 | actual := Config{Name: name} 39 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 40 | return nil, err 41 | } 42 | return actual, nil 43 | } 44 | 45 | // Config defines the configuration for the create-instances tool. 46 | type Config struct { 47 | Name string `yaml:"name" validate:"required"` 48 | Kind string `yaml:"kind" validate:"required"` 49 | Description string `yaml:"description"` 50 | Source string `yaml:"source" validate:"required"` 51 | AuthRequired []string `yaml:"authRequired"` 52 | } 53 | 54 | // validate interface 55 | var _ tools.ToolConfig = Config{} 56 | 57 | // ToolConfigKind returns the kind of the tool. 58 | func (cfg Config) ToolConfigKind() string { 59 | return kind 60 | } 61 | 62 | // Initialize initializes the tool from the configuration. 63 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | s, ok := rawS.(*cloudsqladmin.Source) 69 | if !ok { 70 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `cloud-sql-admin`", kind) 71 | } 72 | 73 | allParameters := tools.Parameters{ 74 | tools.NewStringParameter("project", "The project ID"), 75 | tools.NewStringParameter("name", "The name of the instance"), 76 | tools.NewStringParameterWithDefault("databaseVersion", "MYSQL_8_4", "The database version for MySQL. If not specified, defaults to the latest available version (e.g., MYSQL_8_4)."), 77 | tools.NewStringParameter("rootPassword", "The root password for the instance"), 78 | tools.NewStringParameterWithDefault("editionPreset", "Development", "The edition of the instance. Can be `Production` or `Development`. This determines the default machine type and availability. Defaults to `Development`."), 79 | } 80 | paramManifest := allParameters.Manifest() 81 | 82 | description := cfg.Description 83 | if description == "" { 84 | description = "Creates a MySQL instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 16 GiB RAM, 100 GiB SSD configuration with Non-HA/zonal availability. For the `Production` template, it chooses an 8 vCPU, 64 GiB RAM, 250 GiB SSD configuration with HA/regional availability. The Enterprise Plus edition is used in both cases. The default database version is `MYSQL_8_4`. The agent should ask the user if they want to use a different version." 85 | } 86 | mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters) 87 | 88 | return Tool{ 89 | Name: cfg.Name, 90 | Kind: kind, 91 | AuthRequired: cfg.AuthRequired, 92 | Source: s, 93 | AllParams: allParameters, 94 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 95 | mcpManifest: mcpManifest, 96 | }, nil 97 | } 98 | 99 | // Tool represents the create-instances tool. 100 | type Tool struct { 101 | Name string `yaml:"name"` 102 | Kind string `yaml:"kind"` 103 | Description string `yaml:"description"` 104 | AuthRequired []string `yaml:"authRequired"` 105 | 106 | Source *cloudsqladmin.Source 107 | AllParams tools.Parameters `yaml:"allParams"` 108 | manifest tools.Manifest 109 | mcpManifest tools.McpManifest 110 | } 111 | 112 | // Invoke executes the tool's logic. 113 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 114 | paramsMap := params.AsMap() 115 | 116 | project, ok := paramsMap["project"].(string) 117 | if !ok { 118 | return nil, fmt.Errorf("missing 'project' parameter") 119 | } 120 | name, ok := paramsMap["name"].(string) 121 | if !ok { 122 | return nil, fmt.Errorf("missing 'name' parameter") 123 | } 124 | dbVersion, ok := paramsMap["databaseVersion"].(string) 125 | if !ok { 126 | return nil, fmt.Errorf("missing 'databaseVersion' parameter") 127 | } 128 | rootPassword, ok := paramsMap["rootPassword"].(string) 129 | if !ok { 130 | return nil, fmt.Errorf("missing 'rootPassword' parameter") 131 | } 132 | editionPreset, ok := paramsMap["editionPreset"].(string) 133 | if !ok { 134 | return nil, fmt.Errorf("missing 'editionPreset' parameter") 135 | } 136 | 137 | settings := sqladmin.Settings{} 138 | switch strings.ToLower(editionPreset) { 139 | case "production": 140 | settings.AvailabilityType = "REGIONAL" 141 | settings.Edition = "ENTERPRISE_PLUS" 142 | settings.Tier = "db-perf-optimized-N-8" 143 | settings.DataDiskSizeGb = 250 144 | settings.DataDiskType = "PD_SSD" 145 | case "development": 146 | settings.AvailabilityType = "ZONAL" 147 | settings.Edition = "ENTERPRISE_PLUS" 148 | settings.Tier = "db-perf-optimized-N-2" 149 | settings.DataDiskSizeGb = 100 150 | settings.DataDiskType = "PD_SSD" 151 | default: 152 | return nil, fmt.Errorf("invalid 'editionPreset': %q. Must be either 'Production' or 'Development'", editionPreset) 153 | } 154 | 155 | instance := sqladmin.DatabaseInstance{ 156 | Name: name, 157 | DatabaseVersion: dbVersion, 158 | RootPassword: rootPassword, 159 | Settings: &settings, 160 | Project: project, 161 | } 162 | 163 | service, err := t.Source.GetService(ctx, string(accessToken)) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | resp, err := service.Instances.Insert(project, &instance).Do() 169 | if err != nil { 170 | return nil, fmt.Errorf("error creating instance: %w", err) 171 | } 172 | 173 | return resp, nil 174 | } 175 | 176 | // ParseParams parses the parameters for the tool. 177 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 178 | return tools.ParseParams(t.AllParams, data, claims) 179 | } 180 | 181 | // Manifest returns the tool's manifest. 182 | func (t Tool) Manifest() tools.Manifest { 183 | return t.manifest 184 | } 185 | 186 | // McpManifest returns the tool's MCP manifest. 187 | func (t Tool) McpManifest() tools.McpManifest { 188 | return t.mcpManifest 189 | } 190 | 191 | // Authorized checks if the tool is authorized. 192 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 193 | return true 194 | } 195 | 196 | func (t Tool) RequiresClientAuthorization() bool { 197 | return t.Source.UseClientAuthorization() 198 | } 199 | ``` -------------------------------------------------------------------------------- /internal/tools/cloudsqlpg/cloudsqlpgcreateinstances/cloudsqlpgcreateinstances.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 cloudsqlpgcreateinstances 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | sqladmin "google.golang.org/api/sqladmin/v1" 27 | ) 28 | 29 | const kind string = "cloud-sql-postgres-create-instance" 30 | 31 | func init() { 32 | if !tools.Register(kind, newConfig) { 33 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 34 | } 35 | } 36 | 37 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 38 | actual := Config{Name: name} 39 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 40 | return nil, err 41 | } 42 | return actual, nil 43 | } 44 | 45 | // Config defines the configuration for the create-instances tool. 46 | type Config struct { 47 | Name string `yaml:"name" validate:"required"` 48 | Kind string `yaml:"kind" validate:"required"` 49 | Description string `yaml:"description"` 50 | Source string `yaml:"source" validate:"required"` 51 | AuthRequired []string `yaml:"authRequired"` 52 | } 53 | 54 | // validate interface 55 | var _ tools.ToolConfig = Config{} 56 | 57 | // ToolConfigKind returns the kind of the tool. 58 | func (cfg Config) ToolConfigKind() string { 59 | return kind 60 | } 61 | 62 | // Initialize initializes the tool from the configuration. 63 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | s, ok := rawS.(*cloudsqladmin.Source) 69 | if !ok { 70 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `cloud-sql-admin`", kind) 71 | } 72 | 73 | allParameters := tools.Parameters{ 74 | tools.NewStringParameter("project", "The project ID"), 75 | tools.NewStringParameter("name", "The name of the instance"), 76 | tools.NewStringParameterWithDefault("databaseVersion", "POSTGRES_17", "The database version for Postgres. If not specified, defaults to the latest available version (e.g., POSTGRES_17)."), 77 | tools.NewStringParameter("rootPassword", "The root password for the instance"), 78 | tools.NewStringParameterWithDefault("editionPreset", "Development", "The edition of the instance. Can be `Production` or `Development`. This determines the default machine type and availability. Defaults to `Development`."), 79 | } 80 | paramManifest := allParameters.Manifest() 81 | 82 | description := cfg.Description 83 | if description == "" { 84 | description = "Creates a Postgres instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 16 GiB RAM, 100 GiB SSD configuration with Non-HA/zonal availability. For the `Production` template, it chooses an 8 vCPU, 64 GiB RAM, 250 GiB SSD configuration with HA/regional availability. The Enterprise Plus edition is used in both cases. The default database version is `POSTGRES_17`. The agent should ask the user if they want to use a different version." 85 | } 86 | mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters) 87 | 88 | return Tool{ 89 | Name: cfg.Name, 90 | Kind: kind, 91 | AuthRequired: cfg.AuthRequired, 92 | Source: s, 93 | AllParams: allParameters, 94 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 95 | mcpManifest: mcpManifest, 96 | }, nil 97 | } 98 | 99 | // Tool represents the create-instances tool. 100 | type Tool struct { 101 | Name string `yaml:"name"` 102 | Kind string `yaml:"kind"` 103 | Description string `yaml:"description"` 104 | AuthRequired []string `yaml:"authRequired"` 105 | 106 | Source *cloudsqladmin.Source 107 | AllParams tools.Parameters `yaml:"allParams"` 108 | manifest tools.Manifest 109 | mcpManifest tools.McpManifest 110 | } 111 | 112 | // Invoke executes the tool's logic. 113 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 114 | paramsMap := params.AsMap() 115 | 116 | project, ok := paramsMap["project"].(string) 117 | if !ok { 118 | return nil, fmt.Errorf("missing 'project' parameter") 119 | } 120 | name, ok := paramsMap["name"].(string) 121 | if !ok { 122 | return nil, fmt.Errorf("missing 'name' parameter") 123 | } 124 | dbVersion, ok := paramsMap["databaseVersion"].(string) 125 | if !ok { 126 | return nil, fmt.Errorf("missing 'databaseVersion' parameter") 127 | } 128 | rootPassword, ok := paramsMap["rootPassword"].(string) 129 | if !ok { 130 | return nil, fmt.Errorf("missing 'rootPassword' parameter") 131 | } 132 | editionPreset, ok := paramsMap["editionPreset"].(string) 133 | if !ok { 134 | return nil, fmt.Errorf("missing 'editionPreset' parameter") 135 | } 136 | 137 | settings := sqladmin.Settings{} 138 | switch strings.ToLower(editionPreset) { 139 | case "production": 140 | settings.AvailabilityType = "REGIONAL" 141 | settings.Edition = "ENTERPRISE_PLUS" 142 | settings.Tier = "db-perf-optimized-N-8" 143 | settings.DataDiskSizeGb = 250 144 | settings.DataDiskType = "PD_SSD" 145 | case "development": 146 | settings.AvailabilityType = "ZONAL" 147 | settings.Edition = "ENTERPRISE_PLUS" 148 | settings.Tier = "db-perf-optimized-N-2" 149 | settings.DataDiskSizeGb = 100 150 | settings.DataDiskType = "PD_SSD" 151 | default: 152 | return nil, fmt.Errorf("invalid 'editionPreset': %q. Must be either 'Production' or 'Development'", editionPreset) 153 | } 154 | 155 | instance := sqladmin.DatabaseInstance{ 156 | Name: name, 157 | DatabaseVersion: dbVersion, 158 | RootPassword: rootPassword, 159 | Settings: &settings, 160 | Project: project, 161 | } 162 | 163 | service, err := t.Source.GetService(ctx, string(accessToken)) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | resp, err := service.Instances.Insert(project, &instance).Do() 169 | if err != nil { 170 | return nil, fmt.Errorf("error creating instance: %w", err) 171 | } 172 | 173 | return resp, nil 174 | } 175 | 176 | // ParseParams parses the parameters for the tool. 177 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 178 | return tools.ParseParams(t.AllParams, data, claims) 179 | } 180 | 181 | // Manifest returns the tool's manifest. 182 | func (t Tool) Manifest() tools.Manifest { 183 | return t.manifest 184 | } 185 | 186 | // McpManifest returns the tool's MCP manifest. 187 | func (t Tool) McpManifest() tools.McpManifest { 188 | return t.mcpManifest 189 | } 190 | 191 | // Authorized checks if the tool is authorized. 192 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 193 | return true 194 | } 195 | 196 | func (t Tool) RequiresClientAuthorization() bool { 197 | return t.Source.UseClientAuthorization() 198 | } 199 | ``` -------------------------------------------------------------------------------- /docs/en/how-to/deploy_gke.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Deploy to Kubernetes" 3 | type: docs 4 | weight: 4 5 | description: > 6 | How to set up and configure Toolbox to deploy on Kubernetes with Google Kubernetes Engine (GKE). 7 | --- 8 | 9 | 10 | ## Before you begin 11 | 12 | 1. Set the PROJECT_ID environment variable: 13 | 14 | ```bash 15 | export PROJECT_ID="my-project-id" 16 | ``` 17 | 18 | 1. [Install the `gcloud` CLI](https://cloud.google.com/sdk/docs/install). 19 | 20 | 1. Initialize gcloud CLI: 21 | 22 | ```bash 23 | gcloud init 24 | gcloud config set project $PROJECT_ID 25 | ``` 26 | 27 | 1. You must have the following APIs enabled: 28 | 29 | ```bash 30 | gcloud services enable artifactregistry.googleapis.com \ 31 | cloudbuild.googleapis.com \ 32 | container.googleapis.com \ 33 | iam.googleapis.com 34 | ``` 35 | 36 | 1. `kubectl` is used to manage Kubernetes, the cluster orchestration system used 37 | by GKE. Verify if you have `kubectl` installed: 38 | 39 | ```bash 40 | kubectl version --client 41 | ``` 42 | 43 | 1. If needed, install `kubectl` component using the Google Cloud CLI: 44 | 45 | ```bash 46 | gcloud components install kubectl 47 | ``` 48 | 49 | ## Create a service account 50 | 51 | 1. Specify a name for your service account with an environment variable: 52 | 53 | ```bash 54 | export SA_NAME=toolbox 55 | ``` 56 | 57 | 1. Create a backend service account: 58 | 59 | ```bash 60 | gcloud iam service-accounts create $SA_NAME 61 | ``` 62 | 63 | 1. Grant any IAM roles necessary to the IAM service account. Each source has a 64 | list of necessary IAM permissions listed on its page. The example below is 65 | for cloud sql postgres source: 66 | 67 | ```bash 68 | gcloud projects add-iam-policy-binding $PROJECT_ID \ 69 | --member serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \ 70 | --role roles/cloudsql.client 71 | ``` 72 | 73 | - [AlloyDB IAM Identity](../resources/sources/alloydb-pg.md#iam-permissions) 74 | - [CloudSQL IAM Identity](../resources/sources/cloud-sql-pg.md#iam-permissions) 75 | - [Spanner IAM Identity](../resources/sources/spanner.md#iam-permissions) 76 | 77 | ## Deploy to Kubernetes 78 | 79 | 1. Set environment variables: 80 | 81 | ```bash 82 | export CLUSTER_NAME=toolbox-cluster 83 | export DEPLOYMENT_NAME=toolbox 84 | export SERVICE_NAME=toolbox-service 85 | export REGION=us-central1 86 | export NAMESPACE=toolbox-namespace 87 | export SECRET_NAME=toolbox-config 88 | export KSA_NAME=toolbox-service-account 89 | ``` 90 | 91 | 1. Create a [GKE cluster](https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture). 92 | 93 | ```bash 94 | gcloud container clusters create-auto $CLUSTER_NAME \ 95 | --location=us-central1 96 | ``` 97 | 98 | 1. Get authentication credentials to interact with the cluster. This also 99 | configures `kubectl` to use the cluster. 100 | 101 | ```bash 102 | gcloud container clusters get-credentials $CLUSTER_NAME \ 103 | --region=$REGION \ 104 | --project=$PROJECT_ID 105 | ``` 106 | 107 | 1. View the current context for `kubectl`. 108 | 109 | ```bash 110 | kubectl config current-context 111 | ``` 112 | 113 | 1. Create namespace for the deployment. 114 | 115 | ```bash 116 | kubectl create namespace $NAMESPACE 117 | ``` 118 | 119 | 1. Create a Kubernetes Service Account (KSA). 120 | 121 | ```bash 122 | kubectl create serviceaccount $KSA_NAME --namespace $NAMESPACE 123 | ``` 124 | 125 | 1. Enable the IAM binding between Google Service Account (GSA) and Kubernetes 126 | Service Account (KSA). 127 | 128 | ```bash 129 | gcloud iam service-accounts add-iam-policy-binding \ 130 | --role="roles/iam.workloadIdentityUser" \ 131 | --member="serviceAccount:$PROJECT_ID.svc.id.goog[$NAMESPACE/$KSA_NAME]" \ 132 | $SA_NAME@$PROJECT_ID.iam.gserviceaccount.com 133 | ``` 134 | 135 | 1. Add annotation to KSA to complete binding: 136 | 137 | ```bash 138 | kubectl annotate serviceaccount \ 139 | $KSA_NAME \ 140 | iam.gke.io/gcp-service-account=$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \ 141 | --namespace $NAMESPACE 142 | ``` 143 | 144 | 1. Prepare the Kubernetes secret for your `tools.yaml` file. 145 | 146 | ```bash 147 | kubectl create secret generic $SECRET_NAME \ 148 | --from-file=./tools.yaml \ 149 | --namespace=$NAMESPACE 150 | ``` 151 | 152 | 1. Create a Kubernetes manifest file (`k8s_deployment.yaml`) to build deployment. 153 | 154 | ```yaml 155 | apiVersion: apps/v1 156 | kind: Deployment 157 | metadata: 158 | name: toolbox 159 | namespace: toolbox-namespace 160 | spec: 161 | selector: 162 | matchLabels: 163 | app: toolbox 164 | template: 165 | metadata: 166 | labels: 167 | app: toolbox 168 | spec: 169 | serviceAccountName: toolbox-service-account 170 | containers: 171 | - name: toolbox 172 | # Recommend to use the latest version of toolbox 173 | image: us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest 174 | args: ["--address", "0.0.0.0"] 175 | ports: 176 | - containerPort: 5000 177 | volumeMounts: 178 | - name: toolbox-config 179 | mountPath: "/app/tools.yaml" 180 | subPath: tools.yaml 181 | readOnly: true 182 | volumes: 183 | - name: toolbox-config 184 | secret: 185 | secretName: toolbox-config 186 | items: 187 | - key: tools.yaml 188 | path: tools.yaml 189 | ``` 190 | 191 | 1. Create the deployment. 192 | 193 | ```bash 194 | kubectl apply -f k8s_deployment.yaml --namespace $NAMESPACE 195 | ``` 196 | 197 | 1. Check the status of deployment. 198 | 199 | ```bash 200 | kubectl get deployments --namespace $NAMESPACE 201 | ``` 202 | 203 | 1. Create a Kubernetes manifest file (`k8s_service.yaml`) to build service. 204 | 205 | ```yaml 206 | apiVersion: v1 207 | kind: Service 208 | metadata: 209 | name: toolbox-service 210 | namespace: toolbox-namespace 211 | annotations: 212 | cloud.google.com/l4-rbs: "enabled" 213 | spec: 214 | selector: 215 | app: toolbox 216 | ports: 217 | - port: 5000 218 | targetPort: 5000 219 | type: LoadBalancer 220 | ``` 221 | 222 | 1. Create the service. 223 | 224 | ```bash 225 | kubectl apply -f k8s_service.yaml --namespace $NAMESPACE 226 | ``` 227 | 228 | 1. You can find your IP address created for your service by getting the service 229 | information through the following. 230 | 231 | ```bash 232 | kubectl describe services $SERVICE_NAME --namespace $NAMESPACE 233 | ``` 234 | 235 | 1. To look at logs, run the following. 236 | 237 | ```bash 238 | kubectl logs -f deploy/$DEPLOYMENT_NAME --namespace $NAMESPACE 239 | ``` 240 | 241 | 1. You might have to wait a couple of minutes. It is ready when you can see 242 | `EXTERNAL-IP` with the following command: 243 | 244 | ```bash 245 | kubectl get svc -n $NAMESPACE 246 | ``` 247 | 248 | 1. Access toolbox locally. 249 | 250 | ```bash 251 | curl <EXTERNAL-IP>:5000 252 | ``` 253 | 254 | ## Clean up resources 255 | 256 | 1. Delete secret. 257 | 258 | ```bash 259 | kubectl delete secret $SECRET_NAME --namespace $NAMESPACE 260 | ``` 261 | 262 | 1. Delete deployment. 263 | 264 | ```bash 265 | kubectl delete deployment $DEPLOYMENT_NAME --namespace $NAMESPACE 266 | ``` 267 | 268 | 1. Delete the application's service. 269 | 270 | ```bash 271 | kubectl delete service $SERVICE_NAME --namespace $NAMESPACE 272 | ``` 273 | 274 | 1. Delete the Kubernetes cluster. 275 | 276 | ```bash 277 | gcloud container clusters delete $CLUSTER_NAME \ 278 | --location=$REGION 279 | ``` 280 | ``` -------------------------------------------------------------------------------- /internal/tools/mysql/mysqllisttablesmissinguniqueindexes/mysqllisttablesmissinguniqueindexes.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 mysqllisttablesmissinguniqueindexes 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmysql" 25 | "github.com/googleapis/genai-toolbox/internal/sources/mysql" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlcommon" 28 | "github.com/googleapis/genai-toolbox/internal/util" 29 | ) 30 | 31 | const kind string = "mysql-list-tables-missing-unique-indexes" 32 | 33 | const listTablesMissingUniqueIndexesStatement = ` 34 | SELECT 35 | tab.table_schema AS table_schema, 36 | tab.table_name AS table_name 37 | FROM 38 | information_schema.tables tab 39 | LEFT JOIN 40 | information_schema.table_constraints tco 41 | ON 42 | tab.table_schema = tco.table_schema 43 | AND tab.table_name = tco.table_name 44 | AND tco.constraint_type IN ('PRIMARY KEY', 'UNIQUE') 45 | WHERE 46 | tco.constraint_type IS NULL 47 | AND tab.table_schema NOT IN('mysql', 'information_schema', 'performance_schema', 'sys') 48 | AND tab.table_type = 'BASE TABLE' 49 | AND (COALESCE(?, '') = '' OR tab.table_schema = ?) 50 | ORDER BY 51 | tab.table_schema, 52 | tab.table_name 53 | LIMIT ?; 54 | ` 55 | 56 | func init() { 57 | if !tools.Register(kind, newConfig) { 58 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 59 | } 60 | } 61 | 62 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 63 | actual := Config{Name: name} 64 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 65 | return nil, err 66 | } 67 | return actual, nil 68 | } 69 | 70 | type compatibleSource interface { 71 | MySQLPool() *sql.DB 72 | } 73 | 74 | // validate compatible sources are still compatible 75 | var _ compatibleSource = &mysql.Source{} 76 | var _ compatibleSource = &cloudsqlmysql.Source{} 77 | 78 | var compatibleSources = [...]string{mysql.SourceKind, cloudsqlmysql.SourceKind} 79 | 80 | type Config struct { 81 | Name string `yaml:"name" validate:"required"` 82 | Kind string `yaml:"kind" validate:"required"` 83 | Source string `yaml:"source" validate:"required"` 84 | Description string `yaml:"description" validate:"required"` 85 | AuthRequired []string `yaml:"authRequired"` 86 | } 87 | 88 | // validate interface 89 | var _ tools.ToolConfig = Config{} 90 | 91 | func (cfg Config) ToolConfigKind() string { 92 | return kind 93 | } 94 | 95 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 96 | // verify source exists 97 | rawS, ok := srcs[cfg.Source] 98 | if !ok { 99 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 100 | } 101 | 102 | // verify the source is compatible 103 | s, ok := rawS.(compatibleSource) 104 | if !ok { 105 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 106 | } 107 | 108 | allParameters := tools.Parameters{ 109 | tools.NewStringParameterWithDefault("table_schema", "", "(Optional) The database where the check is to be performed. Check all tables visible to the current user if not specified"), 110 | tools.NewIntParameterWithDefault("limit", 50, "(Optional) Max rows to return, default is 50"), 111 | } 112 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 113 | 114 | // finish tool setup 115 | t := Tool{ 116 | Name: cfg.Name, 117 | Kind: kind, 118 | AuthRequired: cfg.AuthRequired, 119 | Pool: s.MySQLPool(), 120 | allParams: allParameters, 121 | manifest: tools.Manifest{Description: cfg.Description, Parameters: allParameters.Manifest(), AuthRequired: cfg.AuthRequired}, 122 | mcpManifest: mcpManifest, 123 | } 124 | return t, nil 125 | } 126 | 127 | // validate interface 128 | var _ tools.Tool = Tool{} 129 | 130 | type Tool struct { 131 | Name string `yaml:"name"` 132 | Kind string `yaml:"kind"` 133 | AuthRequired []string `yaml:"authRequired"` 134 | allParams tools.Parameters `yaml:"parameters"` 135 | Pool *sql.DB 136 | manifest tools.Manifest 137 | mcpManifest tools.McpManifest 138 | } 139 | 140 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 141 | paramsMap := params.AsMap() 142 | 143 | table_schema, ok := paramsMap["table_schema"].(string) 144 | if !ok { 145 | return nil, fmt.Errorf("invalid 'table_schema' parameter; expected a string") 146 | } 147 | limit, ok := paramsMap["limit"].(int) 148 | if !ok { 149 | return nil, fmt.Errorf("invalid 'limit' parameter; expected an integer") 150 | } 151 | 152 | // Log the query executed for debugging. 153 | logger, err := util.LoggerFromContext(ctx) 154 | if err != nil { 155 | return nil, fmt.Errorf("error getting logger: %s", err) 156 | } 157 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, listTablesMissingUniqueIndexesStatement) 158 | 159 | results, err := t.Pool.QueryContext(ctx, listTablesMissingUniqueIndexesStatement, table_schema, table_schema, limit) 160 | if err != nil { 161 | return nil, fmt.Errorf("unable to execute query: %w", err) 162 | } 163 | defer results.Close() 164 | 165 | cols, err := results.Columns() 166 | if err != nil { 167 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 168 | } 169 | 170 | // create an array of values for each column, which can be re-used to scan each row 171 | rawValues := make([]any, len(cols)) 172 | values := make([]any, len(cols)) 173 | for i := range rawValues { 174 | values[i] = &rawValues[i] 175 | } 176 | 177 | colTypes, err := results.ColumnTypes() 178 | if err != nil { 179 | return nil, fmt.Errorf("unable to get column types: %w", err) 180 | } 181 | 182 | var out []any 183 | for results.Next() { 184 | err := results.Scan(values...) 185 | if err != nil { 186 | return nil, fmt.Errorf("unable to parse row: %w", err) 187 | } 188 | vMap := make(map[string]any) 189 | for i, name := range cols { 190 | val := rawValues[i] 191 | if val == nil { 192 | vMap[name] = nil 193 | continue 194 | } 195 | 196 | vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val) 197 | if err != nil { 198 | return nil, fmt.Errorf("errors encountered when converting values: %w", err) 199 | } 200 | } 201 | out = append(out, vMap) 202 | } 203 | 204 | if err := results.Err(); err != nil { 205 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 206 | } 207 | 208 | return out, nil 209 | } 210 | 211 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 212 | return tools.ParseParams(t.allParams, data, claims) 213 | } 214 | 215 | func (t Tool) Manifest() tools.Manifest { 216 | return t.manifest 217 | } 218 | 219 | func (t Tool) McpManifest() tools.McpManifest { 220 | return t.mcpManifest 221 | } 222 | 223 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 224 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 225 | } 226 | 227 | func (t Tool) RequiresClientAuthorization() bool { 228 | return false 229 | } 230 | ``` -------------------------------------------------------------------------------- /tests/cloudsqlmssql/cloud_sql_mssql_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 cloudsqlmssql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "net/url" 22 | "os" 23 | "regexp" 24 | "slices" 25 | "strings" 26 | "testing" 27 | "time" 28 | 29 | "cloud.google.com/go/cloudsqlconn" 30 | "cloud.google.com/go/cloudsqlconn/sqlserver/mssql" 31 | "github.com/google/uuid" 32 | "github.com/googleapis/genai-toolbox/internal/testutils" 33 | "github.com/googleapis/genai-toolbox/tests" 34 | ) 35 | 36 | var ( 37 | CloudSQLMSSQLSourceKind = "cloud-sql-mssql" 38 | CloudSQLMSSQLToolKind = "mssql-sql" 39 | CloudSQLMSSQLProject = os.Getenv("CLOUD_SQL_MSSQL_PROJECT") 40 | CloudSQLMSSQLRegion = os.Getenv("CLOUD_SQL_MSSQL_REGION") 41 | CloudSQLMSSQLInstance = os.Getenv("CLOUD_SQL_MSSQL_INSTANCE") 42 | CloudSQLMSSQLDatabase = os.Getenv("CLOUD_SQL_MSSQL_DATABASE") 43 | CloudSQLMSSQLIp = os.Getenv("CLOUD_SQL_MSSQL_IP") 44 | CloudSQLMSSQLUser = os.Getenv("CLOUD_SQL_MSSQL_USER") 45 | CloudSQLMSSQLPass = os.Getenv("CLOUD_SQL_MSSQL_PASS") 46 | ) 47 | 48 | func getCloudSQLMSSQLVars(t *testing.T) map[string]any { 49 | switch "" { 50 | case CloudSQLMSSQLProject: 51 | t.Fatal("'CLOUD_SQL_MSSQL_PROJECT' not set") 52 | case CloudSQLMSSQLRegion: 53 | t.Fatal("'CLOUD_SQL_MSSQL_REGION' not set") 54 | case CloudSQLMSSQLInstance: 55 | t.Fatal("'CLOUD_SQL_MSSQL_INSTANCE' not set") 56 | case CloudSQLMSSQLIp: 57 | t.Fatal("'CLOUD_SQL_MSSQL_IP' not set") 58 | case CloudSQLMSSQLDatabase: 59 | t.Fatal("'CLOUD_SQL_MSSQL_DATABASE' not set") 60 | case CloudSQLMSSQLUser: 61 | t.Fatal("'CLOUD_SQL_MSSQL_USER' not set") 62 | case CloudSQLMSSQLPass: 63 | t.Fatal("'CLOUD_SQL_MSSQL_PASS' not set") 64 | } 65 | 66 | return map[string]any{ 67 | "kind": CloudSQLMSSQLSourceKind, 68 | "project": CloudSQLMSSQLProject, 69 | "instance": CloudSQLMSSQLInstance, 70 | "ipAddress": CloudSQLMSSQLIp, 71 | "region": CloudSQLMSSQLRegion, 72 | "database": CloudSQLMSSQLDatabase, 73 | "user": CloudSQLMSSQLUser, 74 | "password": CloudSQLMSSQLPass, 75 | } 76 | } 77 | 78 | // Copied over from cloud_sql_mssql.go 79 | func initCloudSQLMSSQLConnection(project, region, instance, ipAddress, ipType, user, pass, dbname string) (*sql.DB, error) { 80 | // Create dsn 81 | query := fmt.Sprintf("database=%s&cloudsql=%s:%s:%s", dbname, project, region, instance) 82 | url := &url.URL{ 83 | Scheme: "sqlserver", 84 | User: url.UserPassword(user, pass), 85 | Host: ipAddress, 86 | RawQuery: query, 87 | } 88 | 89 | // Get dial options 90 | dialOpts, err := tests.GetCloudSQLDialOpts(ipType) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | // Register sql server driver 96 | if !slices.Contains(sql.Drivers(), "cloudsql-sqlserver-driver") { 97 | _, err := mssql.RegisterDriver("cloudsql-sqlserver-driver", cloudsqlconn.WithDefaultDialOptions(dialOpts...)) 98 | if err != nil { 99 | return nil, err 100 | } 101 | } 102 | 103 | // Open database connection 104 | db, err := sql.Open( 105 | "cloudsql-sqlserver-driver", 106 | url.String(), 107 | ) 108 | if err != nil { 109 | return nil, err 110 | } 111 | return db, nil 112 | } 113 | 114 | func TestCloudSQLMSSQLToolEndpoints(t *testing.T) { 115 | sourceConfig := getCloudSQLMSSQLVars(t) 116 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 117 | defer cancel() 118 | 119 | var args []string 120 | 121 | db, err := initCloudSQLMSSQLConnection(CloudSQLMSSQLProject, CloudSQLMSSQLRegion, CloudSQLMSSQLInstance, CloudSQLMSSQLIp, "public", CloudSQLMSSQLUser, CloudSQLMSSQLPass, CloudSQLMSSQLDatabase) 122 | if err != nil { 123 | t.Fatalf("unable to create Cloud SQL connection pool: %s", err) 124 | } 125 | 126 | // cleanup test environment 127 | tests.CleanupMSSQLTables(t, ctx, db) 128 | 129 | // create table name with UUID 130 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 131 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 132 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 133 | 134 | // set up data for param tool 135 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetMSSQLParamToolInfo(tableNameParam) 136 | teardownTable1 := tests.SetupMsSQLTable(t, ctx, db, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 137 | defer teardownTable1(t) 138 | 139 | // set up data for auth tool 140 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMSSQLAuthToolInfo(tableNameAuth) 141 | teardownTable2 := tests.SetupMsSQLTable(t, ctx, db, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 142 | defer teardownTable2(t) 143 | 144 | // Write config into a file and pass it to command 145 | toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMSSQLToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 146 | toolsFile = tests.AddMSSQLExecuteSqlConfig(t, toolsFile) 147 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetMSSQLTmplToolStatement() 148 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLMSSQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") 149 | toolsFile = tests.AddMSSQLPrebuiltToolConfig(t, toolsFile) 150 | 151 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 152 | if err != nil { 153 | t.Fatalf("command initialization returned an error: %s", err) 154 | } 155 | defer cleanup() 156 | 157 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 158 | defer cancel() 159 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 160 | if err != nil { 161 | t.Logf("toolbox command logs: \n%s", out) 162 | t.Fatalf("toolbox didn't start successfully: %s", err) 163 | } 164 | 165 | // Get configs for tests 166 | select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMSSQLWants() 167 | 168 | // Run tests 169 | tests.RunToolGetTest(t) 170 | tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest()) 171 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 172 | tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) 173 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 174 | 175 | // Run specific MSSQL tool tests 176 | tests.RunMSSQLListTablesTest(t, tableNameParam, tableNameAuth) 177 | } 178 | 179 | // Test connection with different IP type 180 | func TestCloudSQLMSSQLIpConnection(t *testing.T) { 181 | sourceConfig := getCloudSQLMSSQLVars(t) 182 | 183 | tcs := []struct { 184 | name string 185 | ipType string 186 | }{ 187 | { 188 | name: "public ip", 189 | ipType: "public", 190 | }, 191 | { 192 | name: "private ip", 193 | ipType: "private", 194 | }, 195 | } 196 | for _, tc := range tcs { 197 | t.Run(tc.name, func(t *testing.T) { 198 | sourceConfig["ipType"] = tc.ipType 199 | err := tests.RunSourceConnectionTest(t, sourceConfig, CloudSQLMSSQLToolKind) 200 | if err != nil { 201 | t.Fatalf("Connection test failure: %s", err) 202 | } 203 | }) 204 | } 205 | } 206 | ``` -------------------------------------------------------------------------------- /tests/alloydb/alloydb_wait_for_operation_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 alloydb 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httptest" 25 | "net/url" 26 | "reflect" 27 | "regexp" 28 | "strings" 29 | "sync" 30 | "testing" 31 | "time" 32 | 33 | "github.com/googleapis/genai-toolbox/internal/testutils" 34 | "github.com/googleapis/genai-toolbox/tests" 35 | 36 | _ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbwaitforoperation" 37 | ) 38 | 39 | var ( 40 | waitToolKind = "alloydb-wait-for-operation" 41 | ) 42 | 43 | type waitForOperationTransport struct { 44 | transport http.RoundTripper 45 | url *url.URL 46 | } 47 | 48 | func (t *waitForOperationTransport) RoundTrip(req *http.Request) (*http.Response, error) { 49 | if strings.HasPrefix(req.URL.String(), "https://alloydb.googleapis.com") { 50 | req.URL.Scheme = t.url.Scheme 51 | req.URL.Host = t.url.Host 52 | } 53 | return t.transport.RoundTrip(req) 54 | } 55 | 56 | type operation struct { 57 | Name string `json:"name"` 58 | Done bool `json:"done"` 59 | Response any `json:"response,omitempty"` 60 | Error *struct { 61 | Code int `json:"code"` 62 | Message string `json:"message"` 63 | } `json:"error,omitempty"` 64 | } 65 | 66 | type handler struct { 67 | mu sync.Mutex 68 | operations map[string]*operation 69 | t *testing.T 70 | } 71 | 72 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 73 | h.mu.Lock() 74 | defer h.mu.Unlock() 75 | 76 | if !strings.Contains(r.UserAgent(), "genai-toolbox/") { 77 | h.t.Errorf("User-Agent header not found") 78 | } 79 | 80 | // The format is projects/{project}/locations/{location}/operations/{operation} 81 | // The tool will call something like /v1/projects/p1/locations/l1/operations/op1 82 | if match, _ := regexp.MatchString("/v1/projects/.*/locations/.*/operations/.*", r.URL.Path); match { 83 | parts := regexp.MustCompile("/").Split(r.URL.Path, -1) 84 | opName := parts[len(parts)-1] 85 | 86 | op, ok := h.operations[opName] 87 | if !ok { 88 | http.NotFound(w, r) 89 | return 90 | } 91 | 92 | if !op.Done { 93 | op.Done = true 94 | } 95 | 96 | w.Header().Set("Content-Type", "application/json") 97 | if err := json.NewEncoder(w).Encode(op); err != nil { 98 | http.Error(w, err.Error(), http.StatusInternalServerError) 99 | } 100 | } else { 101 | http.NotFound(w, r) 102 | } 103 | } 104 | 105 | func TestWaitToolEndpoints(t *testing.T) { 106 | h := &handler{ 107 | operations: map[string]*operation{ 108 | "op1": {Name: "op1", Done: false, Response: "success"}, 109 | "op2": {Name: "op2", Done: false, Error: &struct { 110 | Code int `json:"code"` 111 | Message string `json:"message"` 112 | }{Code: 1, Message: "failed"}}, 113 | }, 114 | t: t, 115 | } 116 | server := httptest.NewServer(h) 117 | defer server.Close() 118 | 119 | serverURL, err := url.Parse(server.URL) 120 | if err != nil { 121 | t.Fatalf("failed to parse server URL: %v", err) 122 | } 123 | 124 | originalTransport := http.DefaultClient.Transport 125 | if originalTransport == nil { 126 | originalTransport = http.DefaultTransport 127 | } 128 | http.DefaultClient.Transport = &waitForOperationTransport{ 129 | transport: originalTransport, 130 | url: serverURL, 131 | } 132 | t.Cleanup(func() { 133 | http.DefaultClient.Transport = originalTransport 134 | }) 135 | 136 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 137 | defer cancel() 138 | 139 | var args []string 140 | 141 | toolsFile := getWaitToolsConfig() 142 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 143 | if err != nil { 144 | t.Fatalf("command initialization returned an error: %s", err) 145 | } 146 | defer cleanup() 147 | 148 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 149 | defer cancel() 150 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 151 | if err != nil { 152 | t.Logf("toolbox command logs: \n%s", out) 153 | t.Fatalf("toolbox didn't start successfully: %s", err) 154 | } 155 | 156 | tcs := []struct { 157 | name string 158 | toolName string 159 | body string 160 | want string 161 | expectError bool 162 | wantSubstring bool 163 | }{ 164 | { 165 | name: "successful operation", 166 | toolName: "wait-for-op1", 167 | body: `{"project": "p1", "location": "l1", "operation": "op1"}`, 168 | want: `{"name":"op1","done":true,"response":"success"}`, 169 | }, 170 | { 171 | name: "failed operation", 172 | toolName: "wait-for-op2", 173 | body: `{"project": "p1", "location": "l1", "operation": "op2"}`, 174 | expectError: true, 175 | }, 176 | } 177 | 178 | for _, tc := range tcs { 179 | t.Run(tc.name, func(t *testing.T) { 180 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 181 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 182 | if err != nil { 183 | t.Fatalf("unable to create request: %s", err) 184 | } 185 | req.Header.Add("Content-type", "application/json") 186 | resp, err := http.DefaultClient.Do(req) 187 | if err != nil { 188 | t.Fatalf("unable to send request: %s", err) 189 | } 190 | defer resp.Body.Close() 191 | 192 | if tc.expectError { 193 | if resp.StatusCode == http.StatusOK { 194 | t.Fatal("expected error but got status 200") 195 | } 196 | return 197 | } 198 | 199 | if resp.StatusCode != http.StatusOK { 200 | bodyBytes, _ := io.ReadAll(resp.Body) 201 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 202 | } 203 | 204 | var result struct { 205 | Result string `json:"result"` 206 | } 207 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 208 | t.Fatalf("failed to decode response: %v", err) 209 | } 210 | 211 | if tc.wantSubstring { 212 | if !bytes.Contains([]byte(result.Result), []byte(tc.want)) { 213 | t.Fatalf("unexpected result: got %q, want substring %q", result.Result, tc.want) 214 | } 215 | return 216 | } 217 | 218 | // The result is a JSON-encoded string, so we need to unmarshal it twice. 219 | var tempString string 220 | if err := json.Unmarshal([]byte(result.Result), &tempString); err != nil { 221 | t.Fatalf("failed to unmarshal result string: %v", err) 222 | } 223 | 224 | var got, want map[string]any 225 | if err := json.Unmarshal([]byte(tempString), &got); err != nil { 226 | t.Fatalf("failed to unmarshal result: %v", err) 227 | } 228 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 229 | t.Fatalf("failed to unmarshal want: %v", err) 230 | } 231 | 232 | if !reflect.DeepEqual(got, want) { 233 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 234 | } 235 | }) 236 | } 237 | } 238 | 239 | func getWaitToolsConfig() map[string]any { 240 | return map[string]any{ 241 | "sources": map[string]any{ 242 | "my-alloydb-source": map[string]any{ 243 | "kind": "alloydb-admin", 244 | }, 245 | }, 246 | "tools": map[string]any{ 247 | "wait-for-op1": map[string]any{ 248 | "kind": waitToolKind, 249 | "source": "my-alloydb-source", 250 | "description": "wait for op1", 251 | }, 252 | "wait-for-op2": map[string]any{ 253 | "kind": waitToolKind, 254 | "source": "my-alloydb-source", 255 | "description": "wait for op2", 256 | }, 257 | }, 258 | } 259 | } 260 | ``` -------------------------------------------------------------------------------- /internal/tools/alloydb/alloydbcreateinstance/alloydbcreateinstance.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 alloydbcreateinstance 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | alloydbadmin "github.com/googleapis/genai-toolbox/internal/sources/alloydbadmin" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "google.golang.org/api/alloydb/v1" 26 | ) 27 | 28 | const kind string = "alloydb-create-instance" 29 | 30 | func init() { 31 | if !tools.Register(kind, newConfig) { 32 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 33 | } 34 | } 35 | 36 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 37 | actual := Config{Name: name} 38 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 39 | return nil, err 40 | } 41 | return actual, nil 42 | } 43 | 44 | // Configuration for the create-instance tool. 45 | type Config struct { 46 | Name string `yaml:"name" validate:"required"` 47 | Kind string `yaml:"kind" validate:"required"` 48 | Source string `yaml:"source" validate:"required"` 49 | Description string `yaml:"description"` 50 | AuthRequired []string `yaml:"authRequired"` 51 | } 52 | 53 | // validate interface 54 | var _ tools.ToolConfig = Config{} 55 | 56 | // ToolConfigKind returns the kind of the tool. 57 | func (cfg Config) ToolConfigKind() string { 58 | return kind 59 | } 60 | 61 | // Initialize initializes the tool from the configuration. 62 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 63 | rawS, ok := srcs[cfg.Source] 64 | if !ok { 65 | return nil, fmt.Errorf("source %q not found", cfg.Source) 66 | } 67 | 68 | s, ok := rawS.(*alloydbadmin.Source) 69 | if !ok { 70 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `alloydb-admin`", kind) 71 | } 72 | 73 | allParameters := tools.Parameters{ 74 | tools.NewStringParameter("project", "The GCP project ID."), 75 | tools.NewStringParameter("location", "The location of the cluster (e.g., 'us-central1')."), 76 | tools.NewStringParameter("cluster", "The ID of the cluster to create the instance in."), 77 | tools.NewStringParameter("instance", "A unique ID for the new AlloyDB instance."), 78 | tools.NewStringParameterWithDefault("instanceType", "PRIMARY", "The type of instance to create. Valid values are: PRIMARY and READ_POOL. Default is PRIMARY"), 79 | tools.NewStringParameterWithRequired("displayName", "An optional, user-friendly name for the instance.", false), 80 | tools.NewIntParameterWithDefault("nodeCount", 1, "The number of nodes in the read pool. Required only if instanceType is READ_POOL. Default is 1."), 81 | } 82 | paramManifest := allParameters.Manifest() 83 | 84 | description := cfg.Description 85 | if description == "" { 86 | description = "Creates a new AlloyDB instance (PRIMARY or READ_POOL) within a cluster. This is a long-running operation. This will return operation id to be used by get operations tool. Take all parameters from user in one go." 87 | } 88 | mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters) 89 | 90 | return Tool{ 91 | Name: cfg.Name, 92 | Kind: kind, 93 | Source: s, 94 | AllParams: allParameters, 95 | manifest: tools.Manifest{Description: description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 96 | mcpManifest: mcpManifest, 97 | }, nil 98 | } 99 | 100 | // Tool represents the create-instance tool. 101 | type Tool struct { 102 | Name string `yaml:"name"` 103 | Kind string `yaml:"kind"` 104 | Description string `yaml:"description"` 105 | 106 | Source *alloydbadmin.Source 107 | AllParams tools.Parameters `yaml:"allParams"` 108 | 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | } 112 | 113 | // Invoke executes the tool's logic. 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | paramsMap := params.AsMap() 116 | project, ok := paramsMap["project"].(string) 117 | if !ok || project == "" { 118 | return nil, fmt.Errorf("invalid or missing 'project' parameter; expected a non-empty string") 119 | } 120 | 121 | location, ok := paramsMap["location"].(string) 122 | if !ok || location == "" { 123 | return nil, fmt.Errorf("invalid or missing 'location' parameter; expected a non-empty string") 124 | } 125 | 126 | cluster, ok := paramsMap["cluster"].(string) 127 | if !ok || cluster == "" { 128 | return nil, fmt.Errorf("invalid or missing 'cluster' parameter; expected a non-empty string") 129 | } 130 | 131 | instanceID, ok := paramsMap["instance"].(string) 132 | if !ok || instanceID == "" { 133 | return nil, fmt.Errorf("invalid or missing 'instance' parameter; expected a non-empty string") 134 | } 135 | 136 | instanceType, ok := paramsMap["instanceType"].(string) 137 | if !ok || (instanceType != "READ_POOL" && instanceType != "PRIMARY") { 138 | return nil, fmt.Errorf("invalid 'instanceType' parameter; expected 'PRIMARY' or 'READ_POOL'") 139 | } 140 | 141 | service, err := t.Source.GetService(ctx, string(accessToken)) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | urlString := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", project, location, cluster) 147 | 148 | // Build the request body using the type-safe Instance struct. 149 | instance := &alloydb.Instance{ 150 | InstanceType: instanceType, 151 | NetworkConfig: &alloydb.InstanceNetworkConfig{ 152 | EnablePublicIp: true, 153 | }, 154 | DatabaseFlags: map[string]string{ 155 | "password.enforce_complexity": "on", 156 | }, 157 | } 158 | 159 | if displayName, ok := paramsMap["displayName"].(string); ok && displayName != "" { 160 | instance.DisplayName = displayName 161 | } 162 | 163 | if instanceType == "READ_POOL" { 164 | nodeCount, ok := paramsMap["nodeCount"].(int) 165 | if !ok { 166 | return nil, fmt.Errorf("invalid 'nodeCount' parameter; expected an integer for READ_POOL") 167 | } 168 | instance.ReadPoolConfig = &alloydb.ReadPoolConfig{ 169 | NodeCount: int64(nodeCount), 170 | } 171 | } 172 | 173 | // The Create API returns a long-running operation. 174 | resp, err := service.Projects.Locations.Clusters.Instances.Create(urlString, instance).InstanceId(instanceID).Do() 175 | if err != nil { 176 | return nil, fmt.Errorf("error creating AlloyDB instance: %w", err) 177 | } 178 | 179 | return resp, nil 180 | } 181 | 182 | // ParseParams parses the parameters for the tool. 183 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 184 | return tools.ParseParams(t.AllParams, data, claims) 185 | } 186 | 187 | // Manifest returns the tool's manifest. 188 | func (t Tool) Manifest() tools.Manifest { 189 | return t.manifest 190 | } 191 | 192 | // McpManifest returns the tool's MCP manifest. 193 | func (t Tool) McpManifest() tools.McpManifest { 194 | return t.mcpManifest 195 | } 196 | 197 | // Authorized checks if the tool is authorized. 198 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 199 | return true 200 | } 201 | 202 | func (t Tool) RequiresClientAuthorization() bool { 203 | return t.Source.UseClientAuthorization() 204 | } 205 | ``` -------------------------------------------------------------------------------- /internal/tools/firestore/util/converter.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 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 util 16 | 17 | import ( 18 | "encoding/base64" 19 | "fmt" 20 | "strconv" 21 | "strings" 22 | "time" 23 | 24 | "cloud.google.com/go/firestore" 25 | "google.golang.org/genproto/googleapis/type/latlng" 26 | ) 27 | 28 | // JSONToFirestoreValue converts a JSON value with type information to a Firestore-compatible value 29 | // The input should be a map with a single key indicating the type (e.g., "stringValue", "integerValue") 30 | // If a client is provided, referenceValue types will be converted to *firestore.DocumentRef 31 | func JSONToFirestoreValue(value interface{}, client *firestore.Client) (interface{}, error) { 32 | if value == nil { 33 | return nil, nil 34 | } 35 | 36 | switch v := value.(type) { 37 | case map[string]interface{}: 38 | // Check for typed values 39 | if len(v) == 1 { 40 | for key, val := range v { 41 | switch key { 42 | case "nullValue": 43 | return nil, nil 44 | case "booleanValue": 45 | return val, nil 46 | case "stringValue": 47 | return val, nil 48 | case "integerValue": 49 | // Convert to int64 50 | switch num := val.(type) { 51 | case float64: 52 | return int64(num), nil 53 | case int: 54 | return int64(num), nil 55 | case int64: 56 | return num, nil 57 | case string: 58 | // Parse string representation using strconv for better performance 59 | i, err := strconv.ParseInt(strings.TrimSpace(num), 10, 64) 60 | if err != nil { 61 | return nil, fmt.Errorf("invalid integer value: %v", val) 62 | } 63 | return i, nil 64 | } 65 | return nil, fmt.Errorf("invalid integer value: %v", val) 66 | case "doubleValue": 67 | // Convert to float64 68 | switch num := val.(type) { 69 | case float64: 70 | return num, nil 71 | case int: 72 | return float64(num), nil 73 | case int64: 74 | return float64(num), nil 75 | } 76 | return nil, fmt.Errorf("invalid double value: %v", val) 77 | case "bytesValue": 78 | // Decode base64 string to bytes 79 | if str, ok := val.(string); ok { 80 | return base64.StdEncoding.DecodeString(str) 81 | } 82 | return nil, fmt.Errorf("bytes value must be a base64 encoded string") 83 | case "timestampValue": 84 | // Parse timestamp 85 | if str, ok := val.(string); ok { 86 | t, err := time.Parse(time.RFC3339Nano, str) 87 | if err != nil { 88 | return nil, fmt.Errorf("invalid timestamp format: %w", err) 89 | } 90 | return t, nil 91 | } 92 | return nil, fmt.Errorf("timestamp value must be a string") 93 | case "geoPointValue": 94 | // Convert to LatLng 95 | if geoMap, ok := val.(map[string]interface{}); ok { 96 | lat, latOk := geoMap["latitude"].(float64) 97 | lng, lngOk := geoMap["longitude"].(float64) 98 | if latOk && lngOk { 99 | return &latlng.LatLng{ 100 | Latitude: lat, 101 | Longitude: lng, 102 | }, nil 103 | } 104 | } 105 | return nil, fmt.Errorf("invalid geopoint value format") 106 | case "arrayValue": 107 | // Convert array 108 | if arrayMap, ok := val.(map[string]interface{}); ok { 109 | if values, ok := arrayMap["values"].([]interface{}); ok { 110 | result := make([]interface{}, len(values)) 111 | for i, item := range values { 112 | converted, err := JSONToFirestoreValue(item, client) 113 | if err != nil { 114 | return nil, fmt.Errorf("array item %d: %w", i, err) 115 | } 116 | result[i] = converted 117 | } 118 | return result, nil 119 | } 120 | } 121 | return nil, fmt.Errorf("invalid array value format") 122 | case "mapValue": 123 | // Convert map 124 | if mapMap, ok := val.(map[string]interface{}); ok { 125 | if fields, ok := mapMap["fields"].(map[string]interface{}); ok { 126 | result := make(map[string]interface{}) 127 | for k, v := range fields { 128 | converted, err := JSONToFirestoreValue(v, client) 129 | if err != nil { 130 | return nil, fmt.Errorf("map field %q: %w", k, err) 131 | } 132 | result[k] = converted 133 | } 134 | return result, nil 135 | } 136 | } 137 | return nil, fmt.Errorf("invalid map value format") 138 | case "referenceValue": 139 | // Convert to DocumentRef if client is provided 140 | if strVal, ok := val.(string); ok { 141 | if client != nil && isValidDocumentPath(strVal) { 142 | return client.Doc(strVal), nil 143 | } 144 | // Return the path as string if no client or invalid path 145 | return strVal, nil 146 | } 147 | return nil, fmt.Errorf("reference value must be a string") 148 | default: 149 | // If not a typed value, treat as regular map 150 | return convertPlainMap(v, client) 151 | } 152 | } 153 | } 154 | // Regular map without type annotation 155 | return convertPlainMap(v, client) 156 | default: 157 | // Plain values (for backward compatibility) 158 | return value, nil 159 | } 160 | } 161 | 162 | // convertPlainMap converts a plain map to Firestore format 163 | func convertPlainMap(m map[string]interface{}, client *firestore.Client) (map[string]interface{}, error) { 164 | result := make(map[string]interface{}) 165 | for k, v := range m { 166 | converted, err := JSONToFirestoreValue(v, client) 167 | if err != nil { 168 | return nil, fmt.Errorf("field %q: %w", k, err) 169 | } 170 | result[k] = converted 171 | } 172 | return result, nil 173 | } 174 | 175 | // FirestoreValueToJSON converts a Firestore value to a simplified JSON representation 176 | // This removes type information and returns plain values 177 | func FirestoreValueToJSON(value interface{}) interface{} { 178 | if value == nil { 179 | return nil 180 | } 181 | 182 | switch v := value.(type) { 183 | case time.Time: 184 | return v.Format(time.RFC3339Nano) 185 | case *latlng.LatLng: 186 | return map[string]interface{}{ 187 | "latitude": v.Latitude, 188 | "longitude": v.Longitude, 189 | } 190 | case []byte: 191 | return base64.StdEncoding.EncodeToString(v) 192 | case []interface{}: 193 | result := make([]interface{}, len(v)) 194 | for i, item := range v { 195 | result[i] = FirestoreValueToJSON(item) 196 | } 197 | return result 198 | case map[string]interface{}: 199 | result := make(map[string]interface{}) 200 | for k, val := range v { 201 | result[k] = FirestoreValueToJSON(val) 202 | } 203 | return result 204 | case *firestore.DocumentRef: 205 | return v.Path 206 | default: 207 | return value 208 | } 209 | } 210 | 211 | // isValidDocumentPath checks if a string is a valid Firestore document path 212 | // Valid paths have an even number of segments (collection/doc/collection/doc...) 213 | func isValidDocumentPath(path string) bool { 214 | if path == "" { 215 | return false 216 | } 217 | 218 | // Split the path by '/' and check if it has an even number of segments 219 | segments := splitPath(path) 220 | return len(segments) > 0 && len(segments)%2 == 0 221 | } 222 | 223 | // splitPath splits a path by '/' while handling empty segments correctly 224 | func splitPath(path string) []string { 225 | rawSegments := strings.Split(path, "/") 226 | var segments []string 227 | for _, s := range rawSegments { 228 | if s != "" { 229 | segments = append(segments, s) 230 | } 231 | } 232 | return segments 233 | } 234 | ```