#
tokens: 48099/50000 14/807 files (page 21/48)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 21/48FirstPrevNextLast