#
tokens: 48899/50000 13/807 files (page 23/48)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 23 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

--------------------------------------------------------------------------------
/internal/tools/spanner/spannersql/spannersql.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 spannersql
 16 | 
 17 | import (
 18 | 	"context"
 19 | 	"fmt"
 20 | 	"strings"
 21 | 
 22 | 	"cloud.google.com/go/spanner"
 23 | 	yaml "github.com/goccy/go-yaml"
 24 | 	"github.com/googleapis/genai-toolbox/internal/sources"
 25 | 	spannerdb "github.com/googleapis/genai-toolbox/internal/sources/spanner"
 26 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 27 | 	"google.golang.org/api/iterator"
 28 | )
 29 | 
 30 | const kind string = "spanner-sql"
 31 | 
 32 | func init() {
 33 | 	if !tools.Register(kind, newConfig) {
 34 | 		panic(fmt.Sprintf("tool kind %q already registered", kind))
 35 | 	}
 36 | }
 37 | 
 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
 39 | 	actual := Config{Name: name}
 40 | 	if err := decoder.DecodeContext(ctx, &actual); err != nil {
 41 | 		return nil, err
 42 | 	}
 43 | 	return actual, nil
 44 | }
 45 | 
 46 | type compatibleSource interface {
 47 | 	SpannerClient() *spanner.Client
 48 | 	DatabaseDialect() string
 49 | }
 50 | 
 51 | // validate compatible sources are still compatible
 52 | var _ compatibleSource = &spannerdb.Source{}
 53 | 
 54 | var compatibleSources = [...]string{spannerdb.SourceKind}
 55 | 
 56 | type Config struct {
 57 | 	Name               string           `yaml:"name" validate:"required"`
 58 | 	Kind               string           `yaml:"kind" validate:"required"`
 59 | 	Source             string           `yaml:"source" validate:"required"`
 60 | 	Description        string           `yaml:"description" validate:"required"`
 61 | 	Statement          string           `yaml:"statement" validate:"required"`
 62 | 	ReadOnly           bool             `yaml:"readOnly"`
 63 | 	AuthRequired       []string         `yaml:"authRequired"`
 64 | 	Parameters         tools.Parameters `yaml:"parameters"`
 65 | 	TemplateParameters tools.Parameters `yaml:"templateParameters"`
 66 | }
 67 | 
 68 | // validate interface
 69 | var _ tools.ToolConfig = Config{}
 70 | 
 71 | func (cfg Config) ToolConfigKind() string {
 72 | 	return kind
 73 | }
 74 | 
 75 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
 76 | 	// verify source exists
 77 | 	rawS, ok := srcs[cfg.Source]
 78 | 	if !ok {
 79 | 		return nil, fmt.Errorf("no source named %q configured", cfg.Source)
 80 | 	}
 81 | 
 82 | 	// verify the source is compatible
 83 | 	s, ok := rawS.(compatibleSource)
 84 | 	if !ok {
 85 | 		return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
 86 | 	}
 87 | 
 88 | 	allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
 89 | 	if err != nil {
 90 | 		return nil, err
 91 | 	}
 92 | 
 93 | 	mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
 94 | 
 95 | 	// finish tool setup
 96 | 	t := Tool{
 97 | 		Name:               cfg.Name,
 98 | 		Kind:               kind,
 99 | 		Parameters:         cfg.Parameters,
100 | 		TemplateParameters: cfg.TemplateParameters,
101 | 		AllParams:          allParameters,
102 | 		Statement:          cfg.Statement,
103 | 		AuthRequired:       cfg.AuthRequired,
104 | 		ReadOnly:           cfg.ReadOnly,
105 | 		Client:             s.SpannerClient(),
106 | 		dialect:            s.DatabaseDialect(),
107 | 		manifest:           tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
108 | 		mcpManifest:        mcpManifest,
109 | 	}
110 | 	return t, nil
111 | }
112 | 
113 | // validate interface
114 | var _ tools.Tool = Tool{}
115 | 
116 | type Tool struct {
117 | 	Name               string           `yaml:"name"`
118 | 	Kind               string           `yaml:"kind"`
119 | 	AuthRequired       []string         `yaml:"authRequired"`
120 | 	Parameters         tools.Parameters `yaml:"parameters"`
121 | 	TemplateParameters tools.Parameters `yaml:"templateParameters"`
122 | 	AllParams          tools.Parameters `yaml:"allParams"`
123 | 	ReadOnly           bool             `yaml:"readOnly"`
124 | 	Client             *spanner.Client
125 | 	dialect            string
126 | 	Statement          string
127 | 	manifest           tools.Manifest
128 | 	mcpManifest        tools.McpManifest
129 | }
130 | 
131 | func getMapParams(params tools.ParamValues, dialect string) (map[string]interface{}, error) {
132 | 	switch strings.ToLower(dialect) {
133 | 	case "googlesql":
134 | 		return params.AsMap(), nil
135 | 	case "postgresql":
136 | 		return params.AsMapByOrderedKeys(), nil
137 | 	default:
138 | 		return nil, fmt.Errorf("invalid dialect %s", dialect)
139 | 	}
140 | }
141 | 
142 | // processRows iterates over the spanner.RowIterator and converts each row to a map[string]any.
143 | func processRows(iter *spanner.RowIterator) ([]any, error) {
144 | 	var out []any
145 | 	defer iter.Stop()
146 | 
147 | 	for {
148 | 		row, err := iter.Next()
149 | 		if err == iterator.Done {
150 | 			break
151 | 		}
152 | 		if err != nil {
153 | 			return nil, fmt.Errorf("unable to parse row: %w", err)
154 | 		}
155 | 
156 | 		vMap := make(map[string]any)
157 | 		cols := row.ColumnNames()
158 | 		for i, c := range cols {
159 | 			vMap[c] = row.ColumnValue(i)
160 | 		}
161 | 		out = append(out, vMap)
162 | 	}
163 | 	return out, nil
164 | }
165 | 
166 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
167 | 	paramsMap := params.AsMap()
168 | 	newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap)
169 | 	if err != nil {
170 | 		return nil, fmt.Errorf("unable to extract template params %w", err)
171 | 	}
172 | 
173 | 	newParams, err := tools.GetParams(t.Parameters, paramsMap)
174 | 	if err != nil {
175 | 		return nil, fmt.Errorf("unable to extract standard params %w", err)
176 | 	}
177 | 
178 | 	for i, p := range t.Parameters {
179 | 		name := p.GetName()
180 | 		value := newParams[i].Value
181 | 
182 | 		// Spanner only accepts typed slices as input
183 | 		// This checks if the param is an array.
184 | 		// If yes, convert []any to typed slice (e.g []string, []int)
185 | 		switch arrayParam := p.(type) {
186 | 		case *tools.ArrayParameter:
187 | 			arrayParamValue, ok := value.([]any)
188 | 			if !ok {
189 | 				return nil, fmt.Errorf("unable to convert parameter `%s` to []any %w", name, err)
190 | 			}
191 | 			itemType := arrayParam.GetItems().GetType()
192 | 			var err error
193 | 			value, err = tools.ConvertAnySliceToTyped(arrayParamValue, itemType)
194 | 			if err != nil {
195 | 				return nil, fmt.Errorf("unable to convert parameter `%s` from []any to typed slice: %w", name, err)
196 | 			}
197 | 		}
198 | 		newParams[i] = tools.ParamValue{Name: name, Value: value}
199 | 	}
200 | 
201 | 	mapParams, err := getMapParams(newParams, t.dialect)
202 | 	if err != nil {
203 | 		return nil, fmt.Errorf("fail to get map params: %w", err)
204 | 	}
205 | 
206 | 	var results []any
207 | 	var opErr error
208 | 	stmt := spanner.Statement{
209 | 		SQL:    newStatement,
210 | 		Params: mapParams,
211 | 	}
212 | 
213 | 	if t.ReadOnly {
214 | 		iter := t.Client.Single().Query(ctx, stmt)
215 | 		results, opErr = processRows(iter)
216 | 	} else {
217 | 		_, opErr = t.Client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
218 | 			iter := txn.Query(ctx, stmt)
219 | 			results, err = processRows(iter)
220 | 			if err != nil {
221 | 				return err
222 | 			}
223 | 			return nil
224 | 		})
225 | 	}
226 | 
227 | 	if opErr != nil {
228 | 		return nil, fmt.Errorf("unable to execute client: %w", opErr)
229 | 	}
230 | 
231 | 	return results, nil
232 | }
233 | 
234 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
235 | 	return tools.ParseParams(t.AllParams, data, claims)
236 | }
237 | 
238 | func (t Tool) Manifest() tools.Manifest {
239 | 	return t.manifest
240 | }
241 | 
242 | func (t Tool) McpManifest() tools.McpManifest {
243 | 	return t.mcpManifest
244 | }
245 | 
246 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
247 | 	return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
248 | }
249 | 
250 | func (t Tool) RequiresClientAuthorization() bool {
251 | 	return false
252 | }
253 | 
```

--------------------------------------------------------------------------------
/internal/tools/mysql/mysqllisttablefragmentation/mysqllisttablefragmentation.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 mysqllisttablefragmentation
 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-table-fragmentation"
 32 | 
 33 | const listTableFragmentationStatement = `
 34 | 	SELECT
 35 | 		table_schema,
 36 | 		table_name,
 37 | 		data_length AS data_size,
 38 | 		index_length AS index_size,
 39 | 		data_free AS data_free,
 40 | 		ROUND((data_free / (data_length + index_length)) * 100, 2) AS fragmentation_percentage
 41 | 	FROM
 42 | 		information_schema.tables
 43 | 	WHERE
 44 | 		table_schema NOT IN ('sys', 'performance_schema', 'mysql', 'information_schema')
 45 | 		AND (COALESCE(?, '') = '' OR table_schema = ?)
 46 | 		AND (COALESCE(?, '') = '' OR table_name = ?)
 47 | 		AND data_free >= ?
 48 | 	ORDER BY
 49 | 		fragmentation_percentage DESC,
 50 | 		table_schema,
 51 | 		table_name
 52 | 	LIMIT ?;
 53 | `
 54 | 
 55 | func init() {
 56 | 	if !tools.Register(kind, newConfig) {
 57 | 		panic(fmt.Sprintf("tool kind %q already registered", kind))
 58 | 	}
 59 | }
 60 | 
 61 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
 62 | 	actual := Config{Name: name}
 63 | 	if err := decoder.DecodeContext(ctx, &actual); err != nil {
 64 | 		return nil, err
 65 | 	}
 66 | 	return actual, nil
 67 | }
 68 | 
 69 | type compatibleSource interface {
 70 | 	MySQLPool() *sql.DB
 71 | }
 72 | 
 73 | // validate compatible sources are still compatible
 74 | var _ compatibleSource = &mysql.Source{}
 75 | var _ compatibleSource = &cloudsqlmysql.Source{}
 76 | 
 77 | var compatibleSources = [...]string{mysql.SourceKind, cloudsqlmysql.SourceKind}
 78 | 
 79 | type Config struct {
 80 | 	Name         string   `yaml:"name" validate:"required"`
 81 | 	Kind         string   `yaml:"kind" validate:"required"`
 82 | 	Source       string   `yaml:"source" validate:"required"`
 83 | 	Description  string   `yaml:"description" validate:"required"`
 84 | 	AuthRequired []string `yaml:"authRequired"`
 85 | }
 86 | 
 87 | // validate interface
 88 | var _ tools.ToolConfig = Config{}
 89 | 
 90 | func (cfg Config) ToolConfigKind() string {
 91 | 	return kind
 92 | }
 93 | 
 94 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
 95 | 	// verify source exists
 96 | 	rawS, ok := srcs[cfg.Source]
 97 | 	if !ok {
 98 | 		return nil, fmt.Errorf("no source named %q configured", cfg.Source)
 99 | 	}
100 | 
101 | 	// verify the source is compatible
102 | 	s, ok := rawS.(compatibleSource)
103 | 	if !ok {
104 | 		return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
105 | 	}
106 | 
107 | 	allParameters := tools.Parameters{
108 | 		tools.NewStringParameterWithDefault("table_schema", "", "(Optional) The database where fragmentation check is to be executed. Check all tables visible to the current user if not specified"),
109 | 		tools.NewStringParameterWithDefault("table_name", "", "(Optional) Name of the table to be checked. Check all tables visible to the current user if not specified."),
110 | 		tools.NewIntParameterWithDefault("data_free_threshold_bytes", 1, "(Optional) Only show tables with at least this much free space in bytes. Default is 1"),
111 | 		tools.NewIntParameterWithDefault("limit", 10, "(Optional) Max rows to return, default is 10"),
112 | 	}
113 | 	mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
114 | 
115 | 	// finish tool setup
116 | 	t := Tool{
117 | 		Name:         cfg.Name,
118 | 		Kind:         kind,
119 | 		AuthRequired: cfg.AuthRequired,
120 | 		Pool:         s.MySQLPool(),
121 | 		allParams:    allParameters,
122 | 		manifest:     tools.Manifest{Description: cfg.Description, Parameters: allParameters.Manifest(), AuthRequired: cfg.AuthRequired},
123 | 		mcpManifest:  mcpManifest,
124 | 	}
125 | 	return t, nil
126 | }
127 | 
128 | // validate interface
129 | var _ tools.Tool = Tool{}
130 | 
131 | type Tool struct {
132 | 	Name         string           `yaml:"name"`
133 | 	Kind         string           `yaml:"kind"`
134 | 	AuthRequired []string         `yaml:"authRequired"`
135 | 	allParams    tools.Parameters `yaml:"parameters"`
136 | 	Pool         *sql.DB
137 | 	manifest     tools.Manifest
138 | 	mcpManifest  tools.McpManifest
139 | }
140 | 
141 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
142 | 	paramsMap := params.AsMap()
143 | 
144 | 	table_schema, ok := paramsMap["table_schema"].(string)
145 | 	if !ok {
146 | 		return nil, fmt.Errorf("invalid 'table_schema' parameter; expected a string")
147 | 	}
148 | 	table_name, ok := paramsMap["table_name"].(string)
149 | 	if !ok {
150 | 		return nil, fmt.Errorf("invalid 'table_name' parameter; expected a string")
151 | 	}
152 | 	data_free_threshold_bytes, ok := paramsMap["data_free_threshold_bytes"].(int)
153 | 	if !ok {
154 | 		return nil, fmt.Errorf("invalid 'data_free_threshold_bytes' parameter; expected an integer")
155 | 	}
156 | 	limit, ok := paramsMap["limit"].(int)
157 | 	if !ok {
158 | 		return nil, fmt.Errorf("invalid 'limit' parameter; expected an integer")
159 | 	}
160 | 
161 | 	// Log the query executed for debugging.
162 | 	logger, err := util.LoggerFromContext(ctx)
163 | 	if err != nil {
164 | 		return nil, fmt.Errorf("error getting logger: %s", err)
165 | 	}
166 | 	logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, listTableFragmentationStatement)
167 | 
168 | 	results, err := t.Pool.QueryContext(ctx, listTableFragmentationStatement, table_schema, table_schema, table_name, table_name, data_free_threshold_bytes, limit)
169 | 	if err != nil {
170 | 		return nil, fmt.Errorf("unable to execute query: %w", err)
171 | 	}
172 | 	defer results.Close()
173 | 
174 | 	cols, err := results.Columns()
175 | 	if err != nil {
176 | 		return nil, fmt.Errorf("unable to retrieve rows column name: %w", err)
177 | 	}
178 | 
179 | 	// create an array of values for each column, which can be re-used to scan each row
180 | 	rawValues := make([]any, len(cols))
181 | 	values := make([]any, len(cols))
182 | 	for i := range rawValues {
183 | 		values[i] = &rawValues[i]
184 | 	}
185 | 
186 | 	colTypes, err := results.ColumnTypes()
187 | 	if err != nil {
188 | 		return nil, fmt.Errorf("unable to get column types: %w", err)
189 | 	}
190 | 
191 | 	var out []any
192 | 	for results.Next() {
193 | 		err := results.Scan(values...)
194 | 		if err != nil {
195 | 			return nil, fmt.Errorf("unable to parse row: %w", err)
196 | 		}
197 | 		vMap := make(map[string]any)
198 | 		for i, name := range cols {
199 | 			val := rawValues[i]
200 | 			if val == nil {
201 | 				vMap[name] = nil
202 | 				continue
203 | 			}
204 | 
205 | 			vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val)
206 | 			if err != nil {
207 | 				return nil, fmt.Errorf("errors encountered when converting values: %w", err)
208 | 			}
209 | 		}
210 | 		out = append(out, vMap)
211 | 	}
212 | 
213 | 	if err := results.Err(); err != nil {
214 | 		return nil, fmt.Errorf("errors encountered during row iteration: %w", err)
215 | 	}
216 | 
217 | 	return out, nil
218 | }
219 | 
220 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
221 | 	return tools.ParseParams(t.allParams, data, claims)
222 | }
223 | 
224 | func (t Tool) Manifest() tools.Manifest {
225 | 	return t.manifest
226 | }
227 | 
228 | func (t Tool) McpManifest() tools.McpManifest {
229 | 	return t.mcpManifest
230 | }
231 | 
232 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
233 | 	return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
234 | }
235 | 
236 | func (t Tool) RequiresClientAuthorization() bool {
237 | 	return false
238 | }
239 | 
```

--------------------------------------------------------------------------------
/internal/server/static/js/loadTools.js:
--------------------------------------------------------------------------------

```javascript
  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 | import { renderToolInterface } from "./toolDisplay.js";
 16 | 
 17 | let toolDetailsAbortController = null;
 18 | 
 19 | /**
 20 |  * Fetches a toolset from the /api/toolset endpoint and initiates creating the tool list.
 21 |  * @param {!HTMLElement} secondNavContent The HTML element where the tool list will be rendered.
 22 |  * @param {!HTMLElement} toolDisplayArea The HTML element where the details of a selected tool will be displayed.
 23 |  * @param {string} toolsetName The name of the toolset to load (empty string loads all tools).
 24 |  * @returns {!Promise<void>} A promise that resolves when the tools are loaded and rendered, or rejects on error.
 25 |  */
 26 | export async function loadTools(secondNavContent, toolDisplayArea, toolsetName) {
 27 |     secondNavContent.innerHTML = '<p>Fetching tools...</p>';
 28 |     try {
 29 |         const response = await fetch(`/api/toolset/${toolsetName}`);
 30 |         if (!response.ok) {
 31 |             throw new Error(`HTTP error! status: ${response.status}`);
 32 |         }
 33 |         const apiResponse = await response.json();
 34 |         renderToolList(apiResponse, secondNavContent, toolDisplayArea);
 35 |     } catch (error) {
 36 |         console.error('Failed to load tools:', error);
 37 |         secondNavContent.innerHTML = `<p class="error">Failed to load tools: <pre><code>${error}</code></pre></p>`;
 38 |     }
 39 | }
 40 | 
 41 | /**
 42 |  * Renders the list of tools as buttons within the provided HTML element.
 43 |  * @param {?{tools: ?Object<string,*>} } apiResponse The API response object containing the tools.
 44 |  * @param {!HTMLElement} secondNavContent The HTML element to render the tool list into.
 45 |  * @param {!HTMLElement} toolDisplayArea The HTML element for displaying tool details (passed to event handlers).
 46 |  */
 47 | function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
 48 |     secondNavContent.innerHTML = '';
 49 | 
 50 |     if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) {
 51 |         console.error('Error: Expected an object with a "tools" property, but received:', apiResponse);
 52 |         secondNavContent.textContent = 'Error: Invalid response format from toolset API.';
 53 |         return;
 54 |     }
 55 | 
 56 |     const toolsObject = apiResponse.tools;
 57 |     const toolNames = Object.keys(toolsObject);
 58 | 
 59 |     if (toolNames.length === 0) {
 60 |         secondNavContent.textContent = 'No tools found.';
 61 |         return;
 62 |     }
 63 | 
 64 |     const ul = document.createElement('ul');
 65 |     toolNames.forEach(toolName => {
 66 |         const li = document.createElement('li');
 67 |         const button = document.createElement('button');
 68 |         button.textContent = toolName;
 69 |         button.dataset.toolname = toolName;
 70 |         button.classList.add('tool-button');
 71 |         button.addEventListener('click', (event) => handleToolClick(event, secondNavContent, toolDisplayArea));
 72 |         li.appendChild(button);
 73 |         ul.appendChild(li);
 74 |     });
 75 |     secondNavContent.appendChild(ul);
 76 | }
 77 | 
 78 | /**
 79 |  * Handles the click event on a tool button. 
 80 |  * @param {!Event} event The click event object.
 81 |  * @param {!HTMLElement} secondNavContent The parent element containing the tool buttons.
 82 |  * @param {!HTMLElement} toolDisplayArea The HTML element where tool details will be shown.
 83 |  */
 84 | function handleToolClick(event, secondNavContent, toolDisplayArea) {
 85 |     const toolName = event.target.dataset.toolname;
 86 |     if (toolName) {
 87 |         const currentActive = secondNavContent.querySelector('.tool-button.active');
 88 |         if (currentActive) {
 89 |             currentActive.classList.remove('active');
 90 |         }
 91 |         event.target.classList.add('active');
 92 |         fetchToolDetails(toolName, toolDisplayArea);
 93 |     }
 94 | }
 95 | 
 96 | /**
 97 |  * Fetches details for a specific tool /api/tool endpoint.
 98 |  * It aborts any previous in-flight request for tool details to stop race condition.
 99 |  * @param {string} toolName The name of the tool to fetch details for.
100 |  * @param {!HTMLElement} toolDisplayArea The HTML element to display the tool interface in.
101 |  * @returns {!Promise<void>} A promise that resolves when the tool details are fetched and rendered, or rejects on error.
102 |  */
103 | async function fetchToolDetails(toolName, toolDisplayArea) {
104 |     if (toolDetailsAbortController) {
105 |         toolDetailsAbortController.abort();
106 |         console.debug("Aborted previous tool fetch.");
107 |     }
108 | 
109 |     toolDetailsAbortController = new AbortController();
110 |     const signal = toolDetailsAbortController.signal;
111 | 
112 |     toolDisplayArea.innerHTML = '<p>Loading tool details...</p>';
113 | 
114 |     try {
115 |         const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`, { signal });
116 |         if (!response.ok) {
117 |                 throw new Error(`HTTP error! status: ${response.status}`);
118 |         }
119 |         const apiResponse = await response.json();
120 | 
121 |         if (!apiResponse.tools || !apiResponse.tools[toolName]) {
122 |             throw new Error(`Tool "${toolName}" data not found in API response.`);
123 |         }
124 |         const toolObject = apiResponse.tools[toolName];
125 |         console.debug("Received tool object: ", toolObject)
126 | 
127 |         const toolInterfaceData = {
128 |             id: toolName,
129 |             name: toolName,
130 |             description: toolObject.description || "No description provided.",
131 |             authRequired: toolObject.authRequired || [],
132 |             parameters: (toolObject.parameters || []).map(param => {
133 |                 let inputType = 'text'; 
134 |                 const apiType = param.type ? param.type.toLowerCase() : 'string';
135 |                 let valueType = 'string'; 
136 |                 let label = param.description || param.name;
137 | 
138 |                 if (apiType === 'integer' || apiType === 'float') {
139 |                     inputType = 'number';
140 |                     valueType = 'number';
141 |                 } else if (apiType === 'boolean') {
142 |                     inputType = 'checkbox';
143 |                     valueType = 'boolean';
144 |                 } else if (apiType === 'array') {
145 |                     inputType = 'textarea'; 
146 |                     const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
147 |                     valueType = `array<${itemType}>`;
148 |                     label += ' (Array)';
149 |                 }
150 | 
151 |                 return {
152 |                     name: param.name,
153 |                     type: inputType,    
154 |                     valueType: valueType, 
155 |                     label: label,
156 |                     authServices: param.authSources,
157 |                     required: param.required || false,
158 |                     // defaultValue: param.default, can't do this yet bc tool manifest doesn't have default
159 |                 };
160 |             })
161 |         };
162 | 
163 |         console.debug("Transformed toolInterfaceData:", toolInterfaceData);
164 | 
165 |         renderToolInterface(toolInterfaceData, toolDisplayArea);
166 |     } catch (error) {
167 |         if (error.name === 'AbortError') {
168 |             console.debug("Previous fetch was aborted, expected behavior.");
169 |         } else {
170 |             console.error(`Failed to load details for tool "${toolName}":`, error);
171 |             toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${toolName}. ${error.message}</p>`;
172 |         }
173 |     }
174 | }
```

--------------------------------------------------------------------------------
/internal/sources/yugabytedb/yugabytedb_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 yugabytedb_test
 16 | 
 17 | import (
 18 | 	"testing"
 19 | 
 20 | 	"strings"
 21 | 
 22 | 	yaml "github.com/goccy/go-yaml"
 23 | 	"github.com/google/go-cmp/cmp"
 24 | 	"github.com/googleapis/genai-toolbox/internal/server"
 25 | 	"github.com/googleapis/genai-toolbox/internal/sources/yugabytedb"
 26 | 	"github.com/googleapis/genai-toolbox/internal/testutils"
 27 | )
 28 | 
 29 | // Basic config parse
 30 | func TestParseFromYamlYugabyteDB(t *testing.T) {
 31 | 	tcs := []struct {
 32 | 		desc string
 33 | 		in   string
 34 | 		want server.SourceConfigs
 35 | 	}{
 36 | 		{
 37 | 			desc: "only required fields",
 38 | 			in: `
 39 | 			sources:
 40 | 				my-yb-instance:
 41 | 					kind: yugabytedb
 42 | 					name: my-yb-instance
 43 | 					host: yb-host
 44 | 					port: yb-port
 45 | 					user: yb_user
 46 | 					password: yb_pass
 47 | 					database: yb_db
 48 | 			`,
 49 | 			want: server.SourceConfigs{
 50 | 				"my-yb-instance": yugabytedb.Config{
 51 | 					Name:     "my-yb-instance",
 52 | 					Kind:     "yugabytedb",
 53 | 					Host:     "yb-host",
 54 | 					Port:     "yb-port",
 55 | 					User:     "yb_user",
 56 | 					Password: "yb_pass",
 57 | 					Database: "yb_db",
 58 | 				},
 59 | 			},
 60 | 		},
 61 | 		{
 62 | 			desc: "with loadBalance only",
 63 | 			in: `
 64 | 			sources:
 65 | 				my-yb-instance:
 66 | 					kind: yugabytedb
 67 | 					name: my-yb-instance
 68 | 					host: yb-host
 69 | 					port: yb-port
 70 | 					user: yb_user
 71 | 					password: yb_pass
 72 | 					database: yb_db
 73 | 					loadBalance: true
 74 | 			`,
 75 | 			want: server.SourceConfigs{
 76 | 				"my-yb-instance": yugabytedb.Config{
 77 | 					Name:        "my-yb-instance",
 78 | 					Kind:        "yugabytedb",
 79 | 					Host:        "yb-host",
 80 | 					Port:        "yb-port",
 81 | 					User:        "yb_user",
 82 | 					Password:    "yb_pass",
 83 | 					Database:    "yb_db",
 84 | 					LoadBalance: "true",
 85 | 				},
 86 | 			},
 87 | 		},
 88 | 		{
 89 | 			desc: "loadBalance with topologyKeys",
 90 | 			in: `
 91 | 			sources:
 92 | 				my-yb-instance:
 93 | 					kind: yugabytedb
 94 | 					name: my-yb-instance
 95 | 					host: yb-host
 96 | 					port: yb-port
 97 | 					user: yb_user
 98 | 					password: yb_pass
 99 | 					database: yb_db
100 | 					loadBalance: true
101 | 					topologyKeys: zone1,zone2
102 | 			`,
103 | 			want: server.SourceConfigs{
104 | 				"my-yb-instance": yugabytedb.Config{
105 | 					Name:         "my-yb-instance",
106 | 					Kind:         "yugabytedb",
107 | 					Host:         "yb-host",
108 | 					Port:         "yb-port",
109 | 					User:         "yb_user",
110 | 					Password:     "yb_pass",
111 | 					Database:     "yb_db",
112 | 					LoadBalance:  "true",
113 | 					TopologyKeys: "zone1,zone2",
114 | 				},
115 | 			},
116 | 		},
117 | 		{
118 | 			desc: "with fallback only",
119 | 			in: `
120 | 			sources:
121 | 				my-yb-instance:
122 | 					kind: yugabytedb
123 | 					name: my-yb-instance
124 | 					host: yb-host
125 | 					port: yb-port
126 | 					user: yb_user
127 | 					password: yb_pass
128 | 					database: yb_db
129 | 					loadBalance: true
130 | 					topologyKeys: zone1
131 | 					fallbackToTopologyKeysOnly: true
132 | 			`,
133 | 			want: server.SourceConfigs{
134 | 				"my-yb-instance": yugabytedb.Config{
135 | 					Name:                       "my-yb-instance",
136 | 					Kind:                       "yugabytedb",
137 | 					Host:                       "yb-host",
138 | 					Port:                       "yb-port",
139 | 					User:                       "yb_user",
140 | 					Password:                   "yb_pass",
141 | 					Database:                   "yb_db",
142 | 					LoadBalance:                "true",
143 | 					TopologyKeys:               "zone1",
144 | 					FallBackToTopologyKeysOnly: "true",
145 | 				},
146 | 			},
147 | 		},
148 | 		{
149 | 			desc: "with refresh interval and reconnect delay",
150 | 			in: `
151 | 			sources:
152 | 				my-yb-instance:
153 | 					kind: yugabytedb
154 | 					name: my-yb-instance
155 | 					host: yb-host
156 | 					port: yb-port
157 | 					user: yb_user
158 | 					password: yb_pass
159 | 					database: yb_db
160 | 					loadBalance: true
161 | 					ybServersRefreshInterval: 20
162 | 					failedHostReconnectDelaySecs: 5
163 | 			`,
164 | 			want: server.SourceConfigs{
165 | 				"my-yb-instance": yugabytedb.Config{
166 | 					Name:                            "my-yb-instance",
167 | 					Kind:                            "yugabytedb",
168 | 					Host:                            "yb-host",
169 | 					Port:                            "yb-port",
170 | 					User:                            "yb_user",
171 | 					Password:                        "yb_pass",
172 | 					Database:                        "yb_db",
173 | 					LoadBalance:                     "true",
174 | 					YBServersRefreshInterval:        "20",
175 | 					FailedHostReconnectDelaySeconds: "5",
176 | 				},
177 | 			},
178 | 		},
179 | 		{
180 | 			desc: "all fields set",
181 | 			in: `
182 | 			sources:
183 | 				my-yb-instance:
184 | 					kind: yugabytedb
185 | 					name: my-yb-instance
186 | 					host: yb-host
187 | 					port: yb-port
188 | 					user: yb_user
189 | 					password: yb_pass
190 | 					database: yb_db
191 | 					loadBalance: true
192 | 					topologyKeys: zone1,zone2
193 | 					fallbackToTopologyKeysOnly: true
194 | 					ybServersRefreshInterval: 30
195 | 					failedHostReconnectDelaySecs: 10
196 | 			`,
197 | 			want: server.SourceConfigs{
198 | 				"my-yb-instance": yugabytedb.Config{
199 | 					Name:                            "my-yb-instance",
200 | 					Kind:                            "yugabytedb",
201 | 					Host:                            "yb-host",
202 | 					Port:                            "yb-port",
203 | 					User:                            "yb_user",
204 | 					Password:                        "yb_pass",
205 | 					Database:                        "yb_db",
206 | 					LoadBalance:                     "true",
207 | 					TopologyKeys:                    "zone1,zone2",
208 | 					FallBackToTopologyKeysOnly:      "true",
209 | 					YBServersRefreshInterval:        "30",
210 | 					FailedHostReconnectDelaySeconds: "10",
211 | 				},
212 | 			},
213 | 		},
214 | 	}
215 | 
216 | 	for _, tc := range tcs {
217 | 		t.Run(tc.desc, func(t *testing.T) {
218 | 			got := struct {
219 | 				Sources server.SourceConfigs `yaml:"sources"`
220 | 			}{}
221 | 
222 | 			err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
223 | 			if err != nil {
224 | 				t.Fatalf("unable to unmarshal: %s", err)
225 | 			}
226 | 			if !cmp.Equal(tc.want, got.Sources) {
227 | 				t.Fatalf("incorrect parse (-want +got):\n%s", cmp.Diff(tc.want, got.Sources))
228 | 			}
229 | 		})
230 | 	}
231 | }
232 | 
233 | func TestFailParseFromYamlYugabyteDB(t *testing.T) {
234 | 	tcs := []struct {
235 | 		desc string
236 | 		in   string
237 | 		err  string
238 | 	}{
239 | 		{
240 | 			desc: "extra field",
241 | 			in: `
242 | 			sources:
243 | 				my-yb-source:
244 | 					kind: yugabytedb
245 | 					name: my-yb-source
246 | 					host: yb-host
247 | 					port: yb-port
248 | 					database: yb_db
249 | 					user: yb_user
250 | 					password: yb_pass
251 | 					foo: bar
252 | 			`,
253 | 			err: "unable to parse source \"my-yb-source\" as \"yugabytedb\": [2:1] unknown field \"foo\"",
254 | 		},
255 | 		{
256 | 			desc: "missing required field (password)",
257 | 			in: `
258 | 			sources:
259 | 				my-yb-source:
260 | 					kind: yugabytedb
261 | 					name: my-yb-source
262 | 					host: yb-host
263 | 					port: yb-port
264 | 					database: yb_db
265 | 					user: yb_user
266 | 			`,
267 | 			err: "unable to parse source \"my-yb-source\" as \"yugabytedb\": Key: 'Config.Password' Error:Field validation for 'Password' failed on the 'required' tag",
268 | 		},
269 | 		{
270 | 			desc: "missing required field (host)",
271 | 			in: `
272 | 			sources:
273 | 				my-yb-source:
274 | 					kind: yugabytedb
275 | 					name: my-yb-source
276 | 					port: yb-port
277 | 					database: yb_db
278 | 					user: yb_user
279 | 					password: yb_pass
280 | 			`,
281 | 			err: "unable to parse source \"my-yb-source\" as \"yugabytedb\": Key: 'Config.Host' Error:Field validation for 'Host' failed on the 'required' tag",
282 | 		},
283 | 	}
284 | 	for _, tc := range tcs {
285 | 		t.Run(tc.desc, func(t *testing.T) {
286 | 			got := struct {
287 | 				Sources server.SourceConfigs `yaml:"sources"`
288 | 			}{}
289 | 			err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
290 | 			if err == nil {
291 | 				t.Fatalf("expected parsing to fail")
292 | 			}
293 | 			errStr := err.Error()
294 | 			if !strings.Contains(errStr, tc.err) {
295 | 				t.Fatalf("unexpected error:\nGot:  %q\nWant: %q", errStr, tc.err)
296 | 			}
297 | 		})
298 | 	}
299 | }
300 | 
```

--------------------------------------------------------------------------------
/internal/tools/mongodb/mongodbfind/mongodbfind.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 mongodbfind
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"encoding/json"
 19 | 	"fmt"
 20 | 	"slices"
 21 | 
 22 | 	"github.com/goccy/go-yaml"
 23 | 	mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
 24 | 	"github.com/googleapis/genai-toolbox/internal/util"
 25 | 	"go.mongodb.org/mongo-driver/bson"
 26 | 	"go.mongodb.org/mongo-driver/mongo"
 27 | 	"go.mongodb.org/mongo-driver/mongo/options"
 28 | 
 29 | 	"github.com/googleapis/genai-toolbox/internal/sources"
 30 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 31 | )
 32 | 
 33 | const kind string = "mongodb-find"
 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 | 	AuthRequired   []string         `yaml:"authRequired" validate:"required"`
 54 | 	Description    string           `yaml:"description" validate:"required"`
 55 | 	Database       string           `yaml:"database" validate:"required"`
 56 | 	Collection     string           `yaml:"collection" validate:"required"`
 57 | 	FilterPayload  string           `yaml:"filterPayload" validate:"required"`
 58 | 	FilterParams   tools.Parameters `yaml:"filterParams"`
 59 | 	ProjectPayload string           `yaml:"projectPayload"`
 60 | 	ProjectParams  tools.Parameters `yaml:"projectParams"`
 61 | 	SortPayload    string           `yaml:"sortPayload"`
 62 | 	SortParams     tools.Parameters `yaml:"sortParams"`
 63 | 	Limit          int64            `yaml:"limit"`
 64 | }
 65 | 
 66 | // validate interface
 67 | var _ tools.ToolConfig = Config{}
 68 | 
 69 | func (cfg Config) ToolConfigKind() string {
 70 | 	return kind
 71 | }
 72 | 
 73 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
 74 | 	// verify source exists
 75 | 	rawS, ok := srcs[cfg.Source]
 76 | 	if !ok {
 77 | 		return nil, fmt.Errorf("no source named %q configured", cfg.Source)
 78 | 	}
 79 | 
 80 | 	// verify the source is compatible
 81 | 	s, ok := rawS.(*mongosrc.Source)
 82 | 	if !ok {
 83 | 		return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind)
 84 | 	}
 85 | 
 86 | 	// Create a slice for all parameters
 87 | 	allParameters := slices.Concat(cfg.FilterParams, cfg.ProjectParams, cfg.SortParams)
 88 | 
 89 | 	// Verify no duplicate parameter names
 90 | 	err := tools.CheckDuplicateParameters(allParameters)
 91 | 	if err != nil {
 92 | 		return nil, err
 93 | 	}
 94 | 
 95 | 	// Verify 'limit' value
 96 | 	if cfg.Limit <= 0 {
 97 | 		return nil, fmt.Errorf("limit must be a positive number, but got %d", cfg.Limit)
 98 | 	}
 99 | 
100 | 	// Create Toolbox manifest
101 | 	paramManifest := allParameters.Manifest()
102 | 	if paramManifest == nil {
103 | 		paramManifest = make([]tools.ParameterManifest, 0)
104 | 	}
105 | 
106 | 	// Create MCP manifest
107 | 	mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
108 | 
109 | 	// finish tool setup
110 | 	return Tool{
111 | 		Name:           cfg.Name,
112 | 		Kind:           kind,
113 | 		AuthRequired:   cfg.AuthRequired,
114 | 		Collection:     cfg.Collection,
115 | 		FilterPayload:  cfg.FilterPayload,
116 | 		FilterParams:   cfg.FilterParams,
117 | 		ProjectPayload: cfg.ProjectPayload,
118 | 		ProjectParams:  cfg.ProjectParams,
119 | 		SortPayload:    cfg.SortPayload,
120 | 		SortParams:     cfg.SortParams,
121 | 		Limit:          cfg.Limit,
122 | 		AllParams:      allParameters,
123 | 		database:       s.Client.Database(cfg.Database),
124 | 		manifest:       tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
125 | 		mcpManifest:    mcpManifest,
126 | 	}, nil
127 | }
128 | 
129 | // validate interface
130 | var _ tools.Tool = Tool{}
131 | 
132 | type Tool struct {
133 | 	Name           string           `yaml:"name"`
134 | 	Kind           string           `yaml:"kind"`
135 | 	Description    string           `yaml:"description"`
136 | 	AuthRequired   []string         `yaml:"authRequired"`
137 | 	Collection     string           `yaml:"collection"`
138 | 	FilterPayload  string           `yaml:"filterPayload"`
139 | 	FilterParams   tools.Parameters `yaml:"filterParams"`
140 | 	ProjectPayload string           `yaml:"projectPayload"`
141 | 	ProjectParams  tools.Parameters `yaml:"projectParams"`
142 | 	SortPayload    string           `yaml:"sortPayload"`
143 | 	SortParams     tools.Parameters `yaml:"sortParams"`
144 | 	Limit          int64            `yaml:"limit"`
145 | 	AllParams      tools.Parameters `yaml:"allParams"`
146 | 
147 | 	database    *mongo.Database
148 | 	manifest    tools.Manifest
149 | 	mcpManifest tools.McpManifest
150 | }
151 | 
152 | func getOptions(ctx context.Context, sortParameters tools.Parameters, projectPayload string, limit int64, paramsMap map[string]any) (*options.FindOptions, error) {
153 | 	logger, err := util.LoggerFromContext(ctx)
154 | 	if err != nil {
155 | 		panic(err)
156 | 	}
157 | 
158 | 	opts := options.Find()
159 | 
160 | 	sort := bson.M{}
161 | 	for _, p := range sortParameters {
162 | 		sort[p.GetName()] = paramsMap[p.GetName()]
163 | 	}
164 | 	opts = opts.SetSort(sort)
165 | 
166 | 	if len(projectPayload) > 0 {
167 | 
168 | 		result, err := tools.PopulateTemplateWithJSON("MongoDBFindProjectString", projectPayload, paramsMap)
169 | 
170 | 		if err != nil {
171 | 			return nil, fmt.Errorf("error populating project payload: %s", err)
172 | 		}
173 | 
174 | 		var projection any
175 | 		err = bson.UnmarshalExtJSON([]byte(result), false, &projection)
176 | 		if err != nil {
177 | 			return nil, fmt.Errorf("error unmarshalling projection: %s", err)
178 | 		}
179 | 
180 | 		opts = opts.SetProjection(projection)
181 | 		logger.DebugContext(ctx, "Projection is set to %v", projection)
182 | 	}
183 | 
184 | 	if limit > 0 {
185 | 		opts = opts.SetLimit(limit)
186 | 		logger.DebugContext(ctx, "Limit is being set to %d", limit)
187 | 	}
188 | 	return opts, nil
189 | }
190 | 
191 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
192 | 	paramsMap := params.AsMap()
193 | 
194 | 	filterString, err := tools.PopulateTemplateWithJSON("MongoDBFindFilterString", t.FilterPayload, paramsMap)
195 | 
196 | 	if err != nil {
197 | 		return nil, fmt.Errorf("error populating filter: %s", err)
198 | 	}
199 | 
200 | 	opts, err := getOptions(ctx, t.SortParams, t.ProjectPayload, t.Limit, paramsMap)
201 | 	if err != nil {
202 | 		return nil, fmt.Errorf("error populating options: %s", err)
203 | 	}
204 | 
205 | 	var filter = bson.D{}
206 | 	err = bson.UnmarshalExtJSON([]byte(filterString), false, &filter)
207 | 	if err != nil {
208 | 		return nil, err
209 | 	}
210 | 
211 | 	cur, err := t.database.Collection(t.Collection).Find(ctx, filter, opts)
212 | 	if err != nil {
213 | 		return nil, err
214 | 	}
215 | 	defer cur.Close(ctx)
216 | 
217 | 	var data = []any{}
218 | 	err = cur.All(context.TODO(), &data)
219 | 	if err != nil {
220 | 		return nil, err
221 | 	}
222 | 
223 | 	var final []any
224 | 	for _, item := range data {
225 | 		tmp, _ := bson.MarshalExtJSON(item, false, false)
226 | 		var tmp2 any
227 | 		err = json.Unmarshal(tmp, &tmp2)
228 | 		if err != nil {
229 | 			return nil, err
230 | 		}
231 | 		final = append(final, tmp2)
232 | 	}
233 | 
234 | 	return final, err
235 | }
236 | 
237 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
238 | 	return tools.ParseParams(t.AllParams, data, claims)
239 | }
240 | 
241 | func (t Tool) Manifest() tools.Manifest {
242 | 	return t.manifest
243 | }
244 | 
245 | func (t Tool) McpManifest() tools.McpManifest {
246 | 	return t.mcpManifest
247 | }
248 | 
249 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
250 | 	return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
251 | }
252 | 
253 | func (t Tool) RequiresClientAuthorization() bool {
254 | 	return false
255 | }
256 | 
```

--------------------------------------------------------------------------------
/internal/server/static/js/auth.js:
--------------------------------------------------------------------------------

```javascript
  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 | /**
 16 |  * Renders the Google Sign-In button using the GIS library.
 17 |  * @param {string} toolId The ID of the tool.
 18 |  * @param {string} clientId The Google OAuth Client ID.
 19 |  * @param {string} authProfileName The name of the auth service in tools file.
 20 |  */
 21 | function renderGoogleSignInButton(toolId, clientId, authProfileName) { 
 22 |     const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
 23 |     const GIS_CONTAINER_ID = `gisContainer-${UNIQUE_ID_BASE}`;
 24 |     const gisContainer = document.getElementById(GIS_CONTAINER_ID);
 25 |     const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .btn--setup-gis`);
 26 | 
 27 |     if (!gisContainer) {
 28 |         console.error('GIS container not found:', GIS_CONTAINER_ID);
 29 |         return;
 30 |     }
 31 | 
 32 |     if (!clientId) {
 33 |         alert('Please enter an OAuth Client ID first.');
 34 |         return;
 35 |     }
 36 | 
 37 |     // hide the setup button and show the container for the GIS button
 38 |     if (setupGisBtn) setupGisBtn.style.display = 'none';
 39 |     gisContainer.innerHTML = ''; 
 40 |     gisContainer.style.display = 'flex'; 
 41 |     if (window.google && window.google.accounts && window.google.accounts.id) {
 42 |         try {
 43 |             const handleResponse = (response) => handleCredentialResponse(response, toolId, authProfileName);
 44 |             window.google.accounts.id.initialize({
 45 |                 client_id: clientId,
 46 |                 callback: handleResponse,
 47 |                 auto_select: false
 48 |             });
 49 |             window.google.accounts.id.renderButton(
 50 |                 gisContainer,
 51 |                 { theme: "outline", size: "large", text: "signin_with" }
 52 |             );
 53 |         } catch (error) {
 54 |             console.error("Error initializing Google Sign-In:", error);
 55 |             alert("Error initializing Google Sign-In. Check the Client ID and browser console.");
 56 |             gisContainer.innerHTML = '<p style="color: red;">Error loading Sign-In button.</p>';
 57 |             if (setupGisBtn) setupGisBtn.style.display = ''; 
 58 |         }
 59 |     } else {
 60 |         console.error("GIS library not fully loaded yet.");
 61 |         alert("Google Identity Services library not ready. Please try again in a moment.");
 62 |         gisContainer.innerHTML = '<p style="color: red;">GIS library not ready.</p>';
 63 |         if (setupGisBtn) setupGisBtn.style.display = ''; 
 64 |     }
 65 | }
 66 | 
 67 | /**
 68 |  * Handles the credential response from the Google Sign-In library.
 69 |  * @param {!CredentialResponse} response The credential response object from GIS.
 70 |  * @param {string} toolId The ID of the tool.
 71 |  * @param {string} authProfileName The name of the auth service in tools file.
 72 |  */
 73 | function handleCredentialResponse(response, toolId, authProfileName) { 
 74 |     console.debug("handleCredentialResponse called with:", { response, toolId, authProfileName });
 75 |     const headersTextarea = document.getElementById(`headers-textarea-${toolId}`);
 76 |     if (!headersTextarea) {
 77 |         console.error('Headers textarea not found for toolId:', toolId);
 78 |         return;
 79 |     }
 80 | 
 81 |     const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
 82 |     const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .setup-gis-btn`);
 83 |     const gisContainer = document.getElementById(`gisContainer-${UNIQUE_ID_BASE}`);
 84 | 
 85 |     if (response.credential) {
 86 |         const idToken = response.credential;
 87 | 
 88 |         try {
 89 |             let currentHeaders = {};
 90 |             if (headersTextarea.value) {
 91 |                 currentHeaders = JSON.parse(headersTextarea.value);
 92 |             }
 93 |             const HEADER_KEY = `${authProfileName}_token`; 
 94 |             currentHeaders[HEADER_KEY] = `${idToken}`;
 95 |             headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
 96 | 
 97 |             if (gisContainer) gisContainer.style.display = 'none';
 98 |             if (setupGisBtn) setupGisBtn.style.display = '';
 99 | 
100 |         } catch (e) {
101 |             alert('Headers are not valid JSON. Please correct and try again.');
102 |             console.error("Header JSON parse error:", e);
103 |         }
104 |     } else {
105 |         console.error("Error: No credential in response", response);
106 |         alert('Error: No ID Token received. Check console for details.');
107 |         
108 |         if (gisContainer) gisContainer.style.display = 'none';
109 |         if (setupGisBtn) setupGisBtn.style.display = '';
110 |     }
111 | }
112 | 
113 | // creates the Google Auth method dropdown
114 | export function createGoogleAuthMethodItem(toolId, authProfileName) { 
115 |     const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
116 |     const item = document.createElement('div');
117 | 
118 |     item.className = 'auth-method-item';
119 |     item.innerHTML = `
120 |         <div class="auth-method-header">
121 |             <span class="auth-method-label">Google ID Token (${authProfileName})</span>
122 |             <button class="toggle-details-tab">Auto Setup</button>
123 |         </div>
124 |         <div class="auth-method-details" id="google-auth-details-${UNIQUE_ID_BASE}" style="display: none;">
125 |             <div class="auth-controls">
126 |                 <div class="auth-input-row">
127 |                     <label for="clientIdInput-${UNIQUE_ID_BASE}">OAuth Client ID:</label>
128 |                     <input type="text" id="clientIdInput-${UNIQUE_ID_BASE}" placeholder="Enter Client ID" class="auth-input">
129 |                 </div>
130 |                 <div class="auth-instructions">
131 |                     <strong>Action Required:</strong> Add this URL (e.g., http://localhost:PORT) to the Client ID's <strong>Authorized JavaScript origins</strong> to avoid a 401 error. If using Google OAuth, 
132 |                     navigate to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> for this setting.
133 |                 </div>
134 |                 <div class="auth-method-actions">
135 |                     <button class="btn btn--setup-gis">Continue</button>
136 |                     <div id="gisContainer-${UNIQUE_ID_BASE}" class="auth-interactive-element gis-container" style="display: none;"></div>
137 |                 </div>
138 |             </div>
139 |         </div>
140 |     `;
141 | 
142 |     const toggleBtn = item.querySelector('.toggle-details-tab');
143 |     const detailsDiv = item.querySelector(`#google-auth-details-${UNIQUE_ID_BASE}`);
144 |     const setupGisBtn = item.querySelector('.btn--setup-gis');
145 |     const clientIdInput = item.querySelector(`#clientIdInput-${UNIQUE_ID_BASE}`);
146 |     const gisContainer = item.querySelector(`#gisContainer-${UNIQUE_ID_BASE}`);
147 | 
148 |     toggleBtn.addEventListener('click', () => {
149 |         const isVisible = detailsDiv.style.display === 'flex'; 
150 |         detailsDiv.style.display = isVisible ? 'none' : 'flex'; 
151 |         toggleBtn.textContent = isVisible ? 'Auto Setup' : 'Close';
152 |         if (!isVisible) { 
153 |             if (gisContainer) {
154 |                 gisContainer.innerHTML = '';
155 |                 gisContainer.style.display = 'none';
156 |             }
157 |             if (setupGisBtn) {
158 |                 setupGisBtn.style.display = ''; 
159 |             }
160 |         }
161 |     });
162 | 
163 |     setupGisBtn.addEventListener('click', () => {
164 |         const clientId = clientIdInput.value;
165 |         if (!clientId) {
166 |             alert('Please enter an OAuth Client ID first.');
167 |             return;
168 |         }
169 |         renderGoogleSignInButton(toolId, clientId, authProfileName);
170 |     });
171 | 
172 |     return item;
173 | }
174 | 
```

--------------------------------------------------------------------------------
/internal/sources/clickhouse/clickhouse_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 clickhouse
 16 | 
 17 | import (
 18 | 	"context"
 19 | 	"strings"
 20 | 	"testing"
 21 | 
 22 | 	"github.com/goccy/go-yaml"
 23 | 	"github.com/google/go-cmp/cmp"
 24 | 	"github.com/googleapis/genai-toolbox/internal/testutils"
 25 | 	"go.opentelemetry.io/otel"
 26 | )
 27 | 
 28 | func TestConfigSourceConfigKind(t *testing.T) {
 29 | 	config := Config{}
 30 | 	if config.SourceConfigKind() != SourceKind {
 31 | 		t.Errorf("Expected %s, got %s", SourceKind, config.SourceConfigKind())
 32 | 	}
 33 | }
 34 | 
 35 | func TestNewConfig(t *testing.T) {
 36 | 	tests := []struct {
 37 | 		name     string
 38 | 		yaml     string
 39 | 		expected Config
 40 | 	}{
 41 | 		{
 42 | 			name: "all fields specified",
 43 | 			yaml: `
 44 | 				name: test-clickhouse
 45 | 				kind: clickhouse
 46 | 				host: localhost
 47 | 				port: "8443"
 48 | 				user: default
 49 | 				password: "mypass"
 50 | 				database: mydb
 51 | 				protocol: https
 52 | 				secure: true
 53 | 			`,
 54 | 			expected: Config{
 55 | 				Name:     "test-clickhouse",
 56 | 				Kind:     "clickhouse",
 57 | 				Host:     "localhost",
 58 | 				Port:     "8443",
 59 | 				User:     "default",
 60 | 				Password: "mypass",
 61 | 				Database: "mydb",
 62 | 				Protocol: "https",
 63 | 				Secure:   true,
 64 | 			},
 65 | 		},
 66 | 		{
 67 | 			name: "minimal configuration with defaults",
 68 | 			yaml: `
 69 | 				name: minimal-clickhouse
 70 | 				kind: clickhouse
 71 | 				host: 127.0.0.1
 72 | 				port: "8123"
 73 | 				user: testuser
 74 | 				database: testdb
 75 | 			`,
 76 | 			expected: Config{
 77 | 				Name:     "minimal-clickhouse",
 78 | 				Kind:     "clickhouse",
 79 | 				Host:     "127.0.0.1",
 80 | 				Port:     "8123",
 81 | 				User:     "testuser",
 82 | 				Password: "",
 83 | 				Database: "testdb",
 84 | 				Protocol: "",
 85 | 				Secure:   false,
 86 | 			},
 87 | 		},
 88 | 		{
 89 | 			name: "http protocol",
 90 | 			yaml: `
 91 | 				name: http-clickhouse
 92 | 				kind: clickhouse
 93 | 				host: clickhouse.example.com
 94 | 				port: "8123"
 95 | 				user: analytics
 96 | 				password: "securepass"
 97 | 				database: analytics_db
 98 | 				protocol: http
 99 | 				secure: false
100 | 			`,
101 | 			expected: Config{
102 | 				Name:     "http-clickhouse",
103 | 				Kind:     "clickhouse",
104 | 				Host:     "clickhouse.example.com",
105 | 				Port:     "8123",
106 | 				User:     "analytics",
107 | 				Password: "securepass",
108 | 				Database: "analytics_db",
109 | 				Protocol: "http",
110 | 				Secure:   false,
111 | 			},
112 | 		},
113 | 		{
114 | 			name: "https with secure connection",
115 | 			yaml: `
116 | 				name: secure-clickhouse
117 | 				kind: clickhouse
118 | 				host: secure.clickhouse.io
119 | 				port: "8443"
120 | 				user: secureuser
121 | 				password: "verysecure"
122 | 				database: production
123 | 				protocol: https
124 | 				secure: true
125 | 			`,
126 | 			expected: Config{
127 | 				Name:     "secure-clickhouse",
128 | 				Kind:     "clickhouse",
129 | 				Host:     "secure.clickhouse.io",
130 | 				Port:     "8443",
131 | 				User:     "secureuser",
132 | 				Password: "verysecure",
133 | 				Database: "production",
134 | 				Protocol: "https",
135 | 				Secure:   true,
136 | 			},
137 | 		},
138 | 	}
139 | 
140 | 	for _, tt := range tests {
141 | 		t.Run(tt.name, func(t *testing.T) {
142 | 			decoder := yaml.NewDecoder(strings.NewReader(string(testutils.FormatYaml(tt.yaml))))
143 | 			config, err := newConfig(context.Background(), tt.expected.Name, decoder)
144 | 			if err != nil {
145 | 				t.Fatalf("Failed to create config: %v", err)
146 | 			}
147 | 
148 | 			clickhouseConfig, ok := config.(Config)
149 | 			if !ok {
150 | 				t.Fatalf("Expected Config type, got %T", config)
151 | 			}
152 | 
153 | 			if diff := cmp.Diff(tt.expected, clickhouseConfig); diff != "" {
154 | 				t.Errorf("Config mismatch (-want +got):\n%s", diff)
155 | 			}
156 | 		})
157 | 	}
158 | }
159 | 
160 | func TestNewConfigInvalidYAML(t *testing.T) {
161 | 	tests := []struct {
162 | 		name        string
163 | 		yaml        string
164 | 		expectError bool
165 | 	}{
166 | 		{
167 | 			name: "invalid yaml syntax",
168 | 			yaml: `
169 | 				name: test-clickhouse
170 | 				kind: clickhouse
171 | 				host: [invalid
172 | 			`,
173 | 			expectError: true,
174 | 		},
175 | 		{
176 | 			name: "missing required fields",
177 | 			yaml: `
178 | 				name: test-clickhouse
179 | 				kind: clickhouse
180 | 			`,
181 | 			expectError: false,
182 | 		},
183 | 	}
184 | 
185 | 	for _, tt := range tests {
186 | 		t.Run(tt.name, func(t *testing.T) {
187 | 			decoder := yaml.NewDecoder(strings.NewReader(string(testutils.FormatYaml(tt.yaml))))
188 | 			_, err := newConfig(context.Background(), "test-clickhouse", decoder)
189 | 			if tt.expectError && err == nil {
190 | 				t.Errorf("Expected error but got none")
191 | 			}
192 | 			if !tt.expectError && err != nil {
193 | 				t.Errorf("Expected no error but got: %v", err)
194 | 			}
195 | 		})
196 | 	}
197 | }
198 | 
199 | func TestSource_SourceKind(t *testing.T) {
200 | 	source := &Source{}
201 | 	if source.SourceKind() != SourceKind {
202 | 		t.Errorf("Expected %s, got %s", SourceKind, source.SourceKind())
203 | 	}
204 | }
205 | 
206 | func TestValidateConfig(t *testing.T) {
207 | 	tests := []struct {
208 | 		name        string
209 | 		protocol    string
210 | 		expectError bool
211 | 	}{
212 | 		{
213 | 			name:        "valid https protocol",
214 | 			protocol:    "https",
215 | 			expectError: false,
216 | 		},
217 | 		{
218 | 			name:        "valid http protocol",
219 | 			protocol:    "http",
220 | 			expectError: false,
221 | 		},
222 | 		{
223 | 			name:        "invalid protocol",
224 | 			protocol:    "invalid",
225 | 			expectError: true,
226 | 		},
227 | 		{
228 | 			name:        "invalid protocol - native not supported",
229 | 			protocol:    "native",
230 | 			expectError: true,
231 | 		},
232 | 		{
233 | 			name:        "empty values use defaults",
234 | 			protocol:    "",
235 | 			expectError: false,
236 | 		},
237 | 	}
238 | 
239 | 	for _, tt := range tests {
240 | 		t.Run(tt.name, func(t *testing.T) {
241 | 			err := validateConfig(tt.protocol)
242 | 			if tt.expectError && err == nil {
243 | 				t.Errorf("Expected error but got none")
244 | 			}
245 | 			if !tt.expectError && err != nil {
246 | 				t.Errorf("Expected no error but got: %v", err)
247 | 			}
248 | 		})
249 | 	}
250 | }
251 | 
252 | func TestInitClickHouseConnectionPoolDSNGeneration(t *testing.T) {
253 | 	tracer := otel.Tracer("test")
254 | 	ctx := context.Background()
255 | 
256 | 	tests := []struct {
257 | 		name      string
258 | 		host      string
259 | 		port      string
260 | 		user      string
261 | 		pass      string
262 | 		dbname    string
263 | 		protocol  string
264 | 		secure    bool
265 | 		shouldErr bool
266 | 	}{
267 | 		{
268 | 			name:      "http protocol with defaults",
269 | 			host:      "localhost",
270 | 			port:      "8123",
271 | 			user:      "default",
272 | 			pass:      "",
273 | 			dbname:    "default",
274 | 			protocol:  "http",
275 | 			secure:    false,
276 | 			shouldErr: true,
277 | 		},
278 | 		{
279 | 			name:      "https protocol with secure",
280 | 			host:      "localhost",
281 | 			port:      "8443",
282 | 			user:      "default",
283 | 			pass:      "",
284 | 			dbname:    "default",
285 | 			protocol:  "https",
286 | 			secure:    true,
287 | 			shouldErr: true,
288 | 		},
289 | 		{
290 | 			name:      "special characters in password",
291 | 			host:      "localhost",
292 | 			port:      "8443",
293 | 			user:      "test@user",
294 | 			pass:      "pass@word:with/special&chars",
295 | 			dbname:    "default",
296 | 			protocol:  "https",
297 | 			secure:    true,
298 | 			shouldErr: true,
299 | 		},
300 | 		{
301 | 			name:      "invalid protocol should fail",
302 | 			host:      "localhost",
303 | 			port:      "9000",
304 | 			user:      "default",
305 | 			pass:      "",
306 | 			dbname:    "default",
307 | 			protocol:  "invalid",
308 | 			secure:    false,
309 | 			shouldErr: true,
310 | 		},
311 | 		{
312 | 			name:      "empty protocol defaults to https",
313 | 			host:      "localhost",
314 | 			port:      "8443",
315 | 			user:      "user",
316 | 			pass:      "pass",
317 | 			dbname:    "testdb",
318 | 			protocol:  "",
319 | 			secure:    true,
320 | 			shouldErr: true,
321 | 		},
322 | 		{
323 | 			name:      "http with secure flag should upgrade to https",
324 | 			host:      "example.com",
325 | 			port:      "8443",
326 | 			user:      "user",
327 | 			pass:      "pass",
328 | 			dbname:    "db",
329 | 			protocol:  "http",
330 | 			secure:    true,
331 | 			shouldErr: true,
332 | 		},
333 | 	}
334 | 
335 | 	for _, tt := range tests {
336 | 		t.Run(tt.name, func(t *testing.T) {
337 | 			pool, err := initClickHouseConnectionPool(ctx, tracer, "test", tt.host, tt.port, tt.user, tt.pass, tt.dbname, tt.protocol, tt.secure)
338 | 
339 | 			if !tt.shouldErr && err != nil {
340 | 				t.Errorf("Expected no error, got: %v", err)
341 | 			}
342 | 
343 | 			if pool != nil {
344 | 				pool.Close()
345 | 			}
346 | 		})
347 | 	}
348 | }
349 | 
```

--------------------------------------------------------------------------------
/docs/en/getting-started/mcp_quickstart/_index.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: "Quickstart (MCP)"
  3 | type: docs
  4 | weight: 5
  5 | description: >
  6 |   How to get started running Toolbox locally with MCP Inspector.
  7 | ---
  8 | 
  9 | ## Overview
 10 | 
 11 | [Model Context Protocol](https://modelcontextprotocol.io) is an open protocol
 12 | that standardizes how applications provide context to LLMs. Check out this page
 13 | on how to [connect to Toolbox via MCP](../../how-to/connect_via_mcp.md).
 14 | 
 15 | ## Step 1: Set up your database
 16 | 
 17 | In this section, we will create a database, insert some data that needs to be
 18 | access by our agent, and create a database user for Toolbox to connect with.
 19 | 
 20 | 1. Connect to postgres using the `psql` command:
 21 | 
 22 |     ```bash
 23 |     psql -h 127.0.0.1 -U postgres
 24 |     ```
 25 | 
 26 |     Here, `postgres` denotes the default postgres superuser.
 27 | 
 28 | 1. Create a new database and a new user:
 29 | 
 30 |     {{< notice tip >}}
 31 |   For a real application, it's best to follow the principle of least permission
 32 |   and only grant the privileges your application needs.
 33 |     {{< /notice >}}
 34 | 
 35 |     ```sql
 36 |       CREATE USER toolbox_user WITH PASSWORD 'my-password';
 37 | 
 38 |       CREATE DATABASE toolbox_db;
 39 |       GRANT ALL PRIVILEGES ON DATABASE toolbox_db TO toolbox_user;
 40 | 
 41 |       ALTER DATABASE toolbox_db OWNER TO toolbox_user;
 42 |     ```
 43 | 
 44 | 1. End the database session:
 45 | 
 46 |     ```bash
 47 |     \q
 48 |     ```
 49 | 
 50 | 1. Connect to your database with your new user:
 51 | 
 52 |     ```bash
 53 |     psql -h 127.0.0.1 -U toolbox_user -d toolbox_db
 54 |     ```
 55 | 
 56 | 1. Create a table using the following command:
 57 | 
 58 |     ```sql
 59 |     CREATE TABLE hotels(
 60 |       id            INTEGER NOT NULL PRIMARY KEY,
 61 |       name          VARCHAR NOT NULL,
 62 |       location      VARCHAR NOT NULL,
 63 |       price_tier    VARCHAR NOT NULL,
 64 |       checkin_date  DATE    NOT NULL,
 65 |       checkout_date DATE    NOT NULL,
 66 |       booked        BIT     NOT NULL
 67 |     );
 68 |     ```
 69 | 
 70 | 1. Insert data into the table.
 71 | 
 72 |     ```sql
 73 |     INSERT INTO hotels(id, name, location, price_tier, checkin_date, checkout_date, booked)
 74 |     VALUES
 75 |       (1, 'Hilton Basel', 'Basel', 'Luxury', '2024-04-22', '2024-04-20', B'0'),
 76 |       (2, 'Marriott Zurich', 'Zurich', 'Upscale', '2024-04-14', '2024-04-21', B'0'),
 77 |       (3, 'Hyatt Regency Basel', 'Basel', 'Upper Upscale', '2024-04-02', '2024-04-20', B'0'),
 78 |       (4, 'Radisson Blu Lucerne', 'Lucerne', 'Midscale', '2024-04-24', '2024-04-05', B'0'),
 79 |       (5, 'Best Western Bern', 'Bern', 'Upper Midscale', '2024-04-23', '2024-04-01', B'0'),
 80 |       (6, 'InterContinental Geneva', 'Geneva', 'Luxury', '2024-04-23', '2024-04-28', B'0'),
 81 |       (7, 'Sheraton Zurich', 'Zurich', 'Upper Upscale', '2024-04-27', '2024-04-02', B'0'),
 82 |       (8, 'Holiday Inn Basel', 'Basel', 'Upper Midscale', '2024-04-24', '2024-04-09', B'0'),
 83 |       (9, 'Courtyard Zurich', 'Zurich', 'Upscale', '2024-04-03', '2024-04-13', B'0'),
 84 |       (10, 'Comfort Inn Bern', 'Bern', 'Midscale', '2024-04-04', '2024-04-16', B'0');
 85 |     ```
 86 | 
 87 | 1. End the database session:
 88 | 
 89 |     ```bash
 90 |     \q
 91 |     ```
 92 | 
 93 | ## Step 2: Install and configure Toolbox
 94 | 
 95 | In this section, we will download Toolbox, configure our tools in a
 96 | `tools.yaml`, and then run the Toolbox server.
 97 | 
 98 | 1. Download the latest version of Toolbox as a binary:
 99 | 
100 |     {{< notice tip >}}
101 |   Select the
102 |   [correct binary](https://github.com/googleapis/genai-toolbox/releases)
103 |   corresponding to your OS and CPU architecture.
104 |     {{< /notice >}}
105 |     <!-- {x-release-please-start-version} -->
106 |     ```bash
107 |     export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
108 |     curl -O https://storage.googleapis.com/genai-toolbox/v0.17.0/$OS/toolbox
109 |     ```
110 |     <!-- {x-release-please-end} -->
111 | 
112 | 1. Make the binary executable:
113 | 
114 |     ```bash
115 |     chmod +x toolbox
116 |     ```
117 | 
118 | 1. Write the following into a `tools.yaml` file. Be sure to update any fields
119 |    such as `user`, `password`, or `database` that you may have customized in the
120 |    previous step.
121 | 
122 |     {{< notice tip >}}
123 |   In practice, use environment variable replacement with the format ${ENV_NAME}
124 |   instead of hardcoding your secrets into the configuration file.
125 |     {{< /notice >}}
126 | 
127 |     ```yaml
128 |     sources:
129 |       my-pg-source:
130 |         kind: postgres
131 |         host: 127.0.0.1
132 |         port: 5432
133 |         database: toolbox_db
134 |         user: toolbox_user
135 |         password: my-password
136 |     tools:
137 |       search-hotels-by-name:
138 |         kind: postgres-sql
139 |         source: my-pg-source
140 |         description: Search for hotels based on name.
141 |         parameters:
142 |           - name: name
143 |             type: string
144 |             description: The name of the hotel.
145 |         statement: SELECT * FROM hotels WHERE name ILIKE '%' || $1 || '%';
146 |       search-hotels-by-location:
147 |         kind: postgres-sql
148 |         source: my-pg-source
149 |         description: Search for hotels based on location.
150 |         parameters:
151 |           - name: location
152 |             type: string
153 |             description: The location of the hotel.
154 |         statement: SELECT * FROM hotels WHERE location ILIKE '%' || $1 || '%';
155 |       book-hotel:
156 |         kind: postgres-sql
157 |         source: my-pg-source
158 |         description: >-
159 |            Book a hotel by its ID. If the hotel is successfully booked, returns a NULL, raises an error if not.
160 |         parameters:
161 |           - name: hotel_id
162 |             type: string
163 |             description: The ID of the hotel to book.
164 |         statement: UPDATE hotels SET booked = B'1' WHERE id = $1;
165 |       update-hotel:
166 |         kind: postgres-sql
167 |         source: my-pg-source
168 |         description: >-
169 |           Update a hotel's check-in and check-out dates by its ID. Returns a message
170 |           indicating  whether the hotel was successfully updated or not.
171 |         parameters:
172 |           - name: hotel_id
173 |             type: string
174 |             description: The ID of the hotel to update.
175 |           - name: checkin_date
176 |             type: string
177 |             description: The new check-in date of the hotel.
178 |           - name: checkout_date
179 |             type: string
180 |             description: The new check-out date of the hotel.
181 |         statement: >-
182 |           UPDATE hotels SET checkin_date = CAST($2 as date), checkout_date = CAST($3
183 |           as date) WHERE id = $1;
184 |       cancel-hotel:
185 |         kind: postgres-sql
186 |         source: my-pg-source
187 |         description: Cancel a hotel by its ID.
188 |         parameters:
189 |           - name: hotel_id
190 |             type: string
191 |             description: The ID of the hotel to cancel.
192 |         statement: UPDATE hotels SET booked = B'0' WHERE id = $1;
193 |    toolsets:
194 |       my-toolset:
195 |         - search-hotels-by-name
196 |         - search-hotels-by-location
197 |         - book-hotel
198 |         - update-hotel
199 |         - cancel-hotel
200 |     ```
201 | 
202 |     For more info on tools, check out the
203 |     [Tools](../../resources/tools/) section.
204 | 
205 | 1. Run the Toolbox server, pointing to the `tools.yaml` file created earlier:
206 | 
207 |     ```bash
208 |     ./toolbox --tools-file "tools.yaml"
209 |     ```
210 | 
211 | ## Step 3: Connect to MCP Inspector
212 | 
213 | 1. Run the MCP Inspector:
214 | 
215 |     ```bash
216 |     npx @modelcontextprotocol/inspector
217 |     ```
218 | 
219 | 1. Type `y` when it asks to install the inspector package.
220 | 
221 | 1. It should show the following when the MCP Inspector is up and running (please
222 |    take note of `<YOUR_SESSION_TOKEN>`):
223 | 
224 |     ```bash
225 |     Starting MCP inspector...
226 |     ⚙️ Proxy server listening on localhost:6277
227 |     🔑 Session token: <YOUR_SESSION_TOKEN>
228 |        Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth
229 | 
230 |     🚀 MCP Inspector is up and running at:
231 |        http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<YOUR_SESSION_TOKEN>
232 |     ```
233 | 
234 | 1. Open the above link in your browser.
235 | 
236 | 1. For `Transport Type`, select `Streamable HTTP`.
237 | 
238 | 1. For `URL`, type in `http://127.0.0.1:5000/mcp`.
239 | 
240 | 1. For `Configuration` -> `Proxy Session Token`, make sure
241 |    `<YOUR_SESSION_TOKEN>` is present.
242 | 
243 | 1. Click Connect.
244 | 
245 |     ![inspector](./inspector.png)
246 | 
247 | 1. Select `List Tools`, you will see a list of tools configured in `tools.yaml`.
248 | 
249 |     ![inspector_tools](./inspector_tools.png)
250 | 
251 | 1. Test out your tools here!
252 | 
```

--------------------------------------------------------------------------------
/.github/workflows/cloud_build_failure_reporter.yml:
--------------------------------------------------------------------------------

```yaml
  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 | name: Cloud Build Failure Reporter
 16 | 
 17 | on:
 18 |   workflow_call:
 19 |     inputs:
 20 |       trigger_names:
 21 |           required: true
 22 |           type: string
 23 |   workflow_dispatch:
 24 |     inputs:
 25 |       trigger_names:
 26 |         description: 'Cloud Build trigger names separated by comma.'
 27 |         required: true
 28 |         default: ''
 29 | 
 30 | jobs:
 31 |   report:
 32 | 
 33 |     permissions:
 34 |       issues: 'write'
 35 |       checks: 'read'
 36 | 
 37 |     runs-on: 'ubuntu-latest'
 38 | 
 39 |     steps:
 40 |       - uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # v8
 41 |         with:
 42 |           script: |-
 43 |                   // parse test names
 44 |                   const testNameSubstring = '${{ inputs.trigger_names }}';
 45 |                   const testNameFound = new Map(); //keeps track of whether each test is found
 46 |                   testNameSubstring.split(',').forEach(testName => {
 47 |                     testNameFound.set(testName, false); 
 48 |                   });
 49 |                   
 50 |                   // label for all issues opened by reporter
 51 |                   const periodicLabel = 'periodic-failure';
 52 | 
 53 |                   // check if any reporter opened any issues previously
 54 |                   const prevIssues = await github.paginate(github.rest.issues.listForRepo, {
 55 |                     ...context.repo,
 56 |                     state: 'open',
 57 |                     creator: 'github-actions[bot]',
 58 |                     labels: [periodicLabel]
 59 |                   });
 60 | 
 61 |                   // createOrCommentIssue creates a new issue or comments on an existing issue.
 62 |                   const createOrCommentIssue = async function (title, txt) {
 63 |                     if (prevIssues.length < 1) {
 64 |                       console.log('no previous issues found, creating one');
 65 |                       await github.rest.issues.create({
 66 |                         ...context.repo,
 67 |                         title: title,
 68 |                         body: txt,
 69 |                         labels: [periodicLabel]
 70 |                       });
 71 |                       return;
 72 |                     }
 73 |                     // only comment on issue related to the current test
 74 |                     for (const prevIssue of prevIssues) {
 75 |                       if (prevIssue.title.includes(title)){
 76 |                           console.log(
 77 |                           `found previous issue ${prevIssue.html_url}, adding comment`
 78 |                         );
 79 | 
 80 |                         await github.rest.issues.createComment({
 81 |                           ...context.repo,
 82 |                           issue_number: prevIssue.number,
 83 |                           body: txt
 84 |                         });
 85 |                         return;
 86 |                       }
 87 |                     }
 88 |                   };
 89 | 
 90 |                   // updateIssues comments on any existing issues. No-op if no issue exists.
 91 |                   const updateIssues = async function (checkName, txt) {
 92 |                     if (prevIssues.length < 1) {
 93 |                       console.log('no previous issues found.');
 94 |                       return;
 95 |                     }
 96 |                     // only comment on issue related to the current test
 97 |                     for (const prevIssue of prevIssues) {
 98 |                       if (prevIssue.title.includes(checkName)){
 99 |                         console.log(`found previous issue ${prevIssue.html_url}, adding comment`);
100 |                         await github.rest.issues.createComment({
101 |                           ...context.repo,
102 |                           issue_number: prevIssue.number,
103 |                           body: txt
104 |                         });
105 |                       }
106 |                     }
107 |                   };
108 | 
109 |                   // Find status of check runs.
110 |                   // We will find check runs for each commit and then filter for the periodic.
111 |                   // Checks API only allows for ref and if we use main there could be edge cases where
112 |                   // the check run happened on a SHA that is different from head.
113 |                   const commits = await github.paginate(github.rest.repos.listCommits, {
114 |                     ...context.repo
115 |                   });
116 | 
117 |                   const relevantChecks = new Map();
118 |                   for (const commit of commits) {
119 |                     console.log(
120 |                       `checking runs at ${commit.html_url}: ${commit.commit.message}`
121 |                     );
122 |                     const checks = await github.rest.checks.listForRef({
123 |                       ...context.repo,
124 |                       ref: commit.sha
125 |                     });
126 | 
127 |                     // Iterate through each check and find matching names
128 |                     for (const check of checks.data.check_runs) {
129 |                       console.log(`Handling test name ${check.name}`);
130 |                       for (const testName of testNameFound.keys()) {
131 |                         if (testNameFound.get(testName) === true){
132 |                           //skip if a check is already found for this name
133 |                           continue; 
134 |                         }
135 |                         if (check.name.includes(testName)) {
136 |                           relevantChecks.set(check, commit);
137 |                           testNameFound.set(testName, true);
138 |                         }
139 |                       }
140 |                     }
141 |                     // Break out of the loop early if all tests are found
142 |                     const allTestsFound = Array.from(testNameFound.values()).every(value => value === true);
143 |                     if (allTestsFound){
144 |                       break;
145 |                     }
146 |                   }
147 | 
148 |                   // Handle each relevant check
149 |                   relevantChecks.forEach((commit, check) => {
150 |                     if (
151 |                         check.status === 'completed' &&
152 |                         check.conclusion === 'success'
153 |                     ) {
154 |                         updateIssues(
155 |                             check.name,
156 |                             `[Tests are passing](${check.html_url}) for commit [${commit.sha}](${commit.html_url}).`
157 |                         );
158 |                     } else if (check.status === 'in_progress') {
159 |                         console.log(
160 |                             `Check is pending ${check.html_url} for ${commit.html_url}. Retry again later.`
161 |                         );
162 |                     } else {
163 |                         createOrCommentIssue(
164 |                             `Cloud Build Failure Reporter: ${check.name} failed`,
165 |                             `Cloud Build Failure Reporter found test failure for [**${check.name}** ](${check.html_url}) at [${commit.sha}](${commit.html_url}). Please fix the error and then close the issue after the **${check.name}** test passes.`
166 |                         );
167 |                     }
168 |                   });
169 | 
170 |                   // no periodic checks found across all commits, report it
171 |                   const noTestFound = Array.from(testNameFound.values()).every(value => value === false);
172 |                   if (noTestFound){
173 |                     createOrCommentIssue(
174 |                       'Missing periodic tests: ${{ inputs.trigger_names }}',
175 |                       `No periodic test is found for triggers: ${{ inputs.trigger_names }}. Last checked from ${
176 |                         commits[0].html_url
177 |                       } to ${commits[commits.length - 1].html_url}.`
178 |                     );
179 |                   }
180 | 
```

--------------------------------------------------------------------------------
/internal/server/api_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 server
 16 | 
 17 | import (
 18 | 	"bytes"
 19 | 	"encoding/json"
 20 | 	"fmt"
 21 | 	"io"
 22 | 	"net/http"
 23 | 	"strings"
 24 | 	"testing"
 25 | 
 26 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 27 | )
 28 | 
 29 | func TestToolsetEndpoint(t *testing.T) {
 30 | 	mockTools := []MockTool{tool1, tool2}
 31 | 	toolsMap, toolsets := setUpResources(t, mockTools)
 32 | 	r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
 33 | 	defer shutdown()
 34 | 	ts := runServer(r, false)
 35 | 	defer ts.Close()
 36 | 
 37 | 	// wantResponse is a struct for checks against test cases
 38 | 	type wantResponse struct {
 39 | 		statusCode int
 40 | 		isErr      bool
 41 | 		version    string
 42 | 		tools      []string
 43 | 	}
 44 | 
 45 | 	testCases := []struct {
 46 | 		name        string
 47 | 		toolsetName string
 48 | 		want        wantResponse
 49 | 	}{
 50 | 		{
 51 | 			name:        "'default' manifest",
 52 | 			toolsetName: "",
 53 | 			want: wantResponse{
 54 | 				statusCode: http.StatusOK,
 55 | 				version:    fakeVersionString,
 56 | 				tools:      []string{tool1.Name, tool2.Name},
 57 | 			},
 58 | 		},
 59 | 		{
 60 | 			name:        "invalid toolset name",
 61 | 			toolsetName: "some_imaginary_toolset",
 62 | 			want: wantResponse{
 63 | 				statusCode: http.StatusNotFound,
 64 | 				isErr:      true,
 65 | 			},
 66 | 		},
 67 | 		{
 68 | 			name:        "single toolset 1",
 69 | 			toolsetName: "tool1_only",
 70 | 			want: wantResponse{
 71 | 				statusCode: http.StatusOK,
 72 | 				version:    fakeVersionString,
 73 | 				tools:      []string{tool1.Name},
 74 | 			},
 75 | 		},
 76 | 		{
 77 | 			name:        "single toolset 2",
 78 | 			toolsetName: "tool2_only",
 79 | 			want: wantResponse{
 80 | 				statusCode: http.StatusOK,
 81 | 				version:    fakeVersionString,
 82 | 				tools:      []string{tool2.Name},
 83 | 			},
 84 | 		},
 85 | 	}
 86 | 
 87 | 	for _, tc := range testCases {
 88 | 		t.Run(tc.name, func(t *testing.T) {
 89 | 			resp, body, err := runRequest(ts, http.MethodGet, fmt.Sprintf("/toolset/%s", tc.toolsetName), nil, nil)
 90 | 			if err != nil {
 91 | 				t.Fatalf("unexpected error during request: %s", err)
 92 | 			}
 93 | 
 94 | 			if contentType := resp.Header.Get("Content-type"); contentType != "application/json" {
 95 | 				t.Fatalf("unexpected content-type header: want %s, got %s", "application/json", contentType)
 96 | 			}
 97 | 
 98 | 			if resp.StatusCode != tc.want.statusCode {
 99 | 				t.Logf("response body: %s", body)
100 | 				t.Fatalf("unexpected status code: want %d, got %d", tc.want.statusCode, resp.StatusCode)
101 | 			}
102 | 			if tc.want.isErr {
103 | 				// skip the rest of the checks if this is an error case
104 | 				return
105 | 			}
106 | 			var m tools.ToolsetManifest
107 | 			err = json.Unmarshal(body, &m)
108 | 			if err != nil {
109 | 				t.Fatalf("unable to parse ToolsetManifest: %s", err)
110 | 			}
111 | 			// Check the version is correct
112 | 			if m.ServerVersion != tc.want.version {
113 | 				t.Fatalf("unexpected ServerVersion: want %q, got %q", tc.want.version, m.ServerVersion)
114 | 			}
115 | 			// validate that the tools in the toolset are correct
116 | 			for _, name := range tc.want.tools {
117 | 				_, ok := m.ToolsManifest[name]
118 | 				if !ok {
119 | 					t.Errorf("%q tool not found in manifest", name)
120 | 				}
121 | 			}
122 | 		})
123 | 	}
124 | }
125 | 
126 | func TestToolGetEndpoint(t *testing.T) {
127 | 	mockTools := []MockTool{tool1, tool2}
128 | 	toolsMap, toolsets := setUpResources(t, mockTools)
129 | 	r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
130 | 	defer shutdown()
131 | 	ts := runServer(r, false)
132 | 	defer ts.Close()
133 | 
134 | 	// wantResponse is a struct for checks against test cases
135 | 	type wantResponse struct {
136 | 		statusCode int
137 | 		isErr      bool
138 | 		version    string
139 | 		tools      []string
140 | 	}
141 | 
142 | 	testCases := []struct {
143 | 		name     string
144 | 		toolName string
145 | 		want     wantResponse
146 | 	}{
147 | 		{
148 | 			name:     "tool1",
149 | 			toolName: tool1.Name,
150 | 			want: wantResponse{
151 | 				statusCode: http.StatusOK,
152 | 				version:    fakeVersionString,
153 | 				tools:      []string{tool1.Name},
154 | 			},
155 | 		},
156 | 		{
157 | 			name:     "tool2",
158 | 			toolName: tool2.Name,
159 | 			want: wantResponse{
160 | 				statusCode: http.StatusOK,
161 | 				version:    fakeVersionString,
162 | 				tools:      []string{tool2.Name},
163 | 			},
164 | 		},
165 | 		{
166 | 			name:     "invalid tool",
167 | 			toolName: "some_imaginary_tool",
168 | 			want: wantResponse{
169 | 				statusCode: http.StatusNotFound,
170 | 				isErr:      true,
171 | 			},
172 | 		},
173 | 	}
174 | 
175 | 	for _, tc := range testCases {
176 | 		t.Run(tc.name, func(t *testing.T) {
177 | 			resp, body, err := runRequest(ts, http.MethodGet, fmt.Sprintf("/tool/%s", tc.toolName), nil, nil)
178 | 			if err != nil {
179 | 				t.Fatalf("unexpected error during request: %s", err)
180 | 			}
181 | 
182 | 			if contentType := resp.Header.Get("Content-type"); contentType != "application/json" {
183 | 				t.Fatalf("unexpected content-type header: want %s, got %s", "application/json", contentType)
184 | 			}
185 | 
186 | 			if resp.StatusCode != tc.want.statusCode {
187 | 				t.Logf("response body: %s", body)
188 | 				t.Fatalf("unexpected status code: want %d, got %d", tc.want.statusCode, resp.StatusCode)
189 | 			}
190 | 			if tc.want.isErr {
191 | 				// skip the rest of the checks if this is an error case
192 | 				return
193 | 			}
194 | 			var m tools.ToolsetManifest
195 | 			err = json.Unmarshal(body, &m)
196 | 			if err != nil {
197 | 				t.Fatalf("unable to parse ToolsetManifest: %s", err)
198 | 			}
199 | 			// Check the version is correct
200 | 			if m.ServerVersion != tc.want.version {
201 | 				t.Fatalf("unexpected ServerVersion: want %q, got %q", tc.want.version, m.ServerVersion)
202 | 			}
203 | 			// validate that the tools in the toolset are correct
204 | 			for _, name := range tc.want.tools {
205 | 				_, ok := m.ToolsManifest[name]
206 | 				if !ok {
207 | 					t.Errorf("%q tool not found in manifest", name)
208 | 				}
209 | 			}
210 | 		})
211 | 	}
212 | }
213 | 
214 | func TestToolInvokeEndpoint(t *testing.T) {
215 | 	mockTools := []MockTool{tool1, tool2, tool4, tool5}
216 | 	toolsMap, toolsets := setUpResources(t, mockTools)
217 | 	r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
218 | 	defer shutdown()
219 | 	ts := runServer(r, false)
220 | 	defer ts.Close()
221 | 
222 | 	testCases := []struct {
223 | 		name        string
224 | 		toolName    string
225 | 		requestBody io.Reader
226 | 		want        string
227 | 		isErr       bool
228 | 	}{
229 | 		{
230 | 			name:        "tool1",
231 | 			toolName:    tool1.Name,
232 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
233 | 			want:        "{result:[no_params]}\n",
234 | 			isErr:       false,
235 | 		},
236 | 		{
237 | 			name:        "tool2",
238 | 			toolName:    tool2.Name,
239 | 			requestBody: bytes.NewBuffer([]byte(`{"param1": 1, "param2": 2}`)),
240 | 			want:        "{result:[some_params]}\n",
241 | 			isErr:       false,
242 | 		},
243 | 		{
244 | 			name:        "invalid tool",
245 | 			toolName:    "some_imaginary_tool",
246 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
247 | 			want:        "",
248 | 			isErr:       true,
249 | 		},
250 | 		{
251 | 			name:        "tool4",
252 | 			toolName:    tool4.Name,
253 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
254 | 			want:        "",
255 | 			isErr:       true,
256 | 		},
257 | 		{
258 | 			name:        "tool5",
259 | 			toolName:    tool5.Name,
260 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
261 | 			want:        "",
262 | 			isErr:       true,
263 | 		},
264 | 	}
265 | 
266 | 	for _, tc := range testCases {
267 | 		t.Run(tc.name, func(t *testing.T) {
268 | 			resp, body, err := runRequest(ts, http.MethodPost, fmt.Sprintf("/tool/%s/invoke", tc.toolName), tc.requestBody, nil)
269 | 			if err != nil {
270 | 				t.Fatalf("unexpected error during request: %s", err)
271 | 			}
272 | 
273 | 			if contentType := resp.Header.Get("Content-type"); contentType != "application/json" {
274 | 				t.Fatalf("unexpected content-type header: want %s, got %s", "application/json", contentType)
275 | 			}
276 | 
277 | 			if resp.StatusCode != http.StatusOK {
278 | 				if tc.isErr == true {
279 | 					return
280 | 				}
281 | 				t.Fatalf("response status code is not 200, got %d, %s", resp.StatusCode, string(body))
282 | 			}
283 | 
284 | 			got := string(body)
285 | 
286 | 			// Remove `\` and `"` for string comparison
287 | 			got = strings.ReplaceAll(got, "\\", "")
288 | 			want := strings.ReplaceAll(tc.want, "\\", "")
289 | 			got = strings.ReplaceAll(got, "\"", "")
290 | 			want = strings.ReplaceAll(want, "\"", "")
291 | 
292 | 			if got != want {
293 | 				t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
294 | 			}
295 | 		})
296 | 	}
297 | }
298 | 
```

--------------------------------------------------------------------------------
/internal/server/config.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 | package server
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"fmt"
 19 | 	"strings"
 20 | 
 21 | 	yaml "github.com/goccy/go-yaml"
 22 | 	"github.com/googleapis/genai-toolbox/internal/auth"
 23 | 	"github.com/googleapis/genai-toolbox/internal/auth/google"
 24 | 	"github.com/googleapis/genai-toolbox/internal/sources"
 25 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 26 | 	"github.com/googleapis/genai-toolbox/internal/util"
 27 | )
 28 | 
 29 | type ServerConfig struct {
 30 | 	// Server version
 31 | 	Version string
 32 | 	// Address is the address of the interface the server will listen on.
 33 | 	Address string
 34 | 	// Port is the port the server will listen on.
 35 | 	Port int
 36 | 	// SourceConfigs defines what sources of data are available for tools.
 37 | 	SourceConfigs SourceConfigs
 38 | 	// AuthServiceConfigs defines what sources of authentication are available for tools.
 39 | 	AuthServiceConfigs AuthServiceConfigs
 40 | 	// ToolConfigs defines what tools are available.
 41 | 	ToolConfigs ToolConfigs
 42 | 	// ToolsetConfigs defines what tools are available.
 43 | 	ToolsetConfigs ToolsetConfigs
 44 | 	// LoggingFormat defines whether structured loggings are used.
 45 | 	LoggingFormat logFormat
 46 | 	// LogLevel defines the levels to log.
 47 | 	LogLevel StringLevel
 48 | 	// TelemetryGCP defines whether GCP exporter is used.
 49 | 	TelemetryGCP bool
 50 | 	// TelemetryOTLP defines OTLP collector url for telemetry exports.
 51 | 	TelemetryOTLP string
 52 | 	// TelemetryServiceName defines the value of service.name resource attribute.
 53 | 	TelemetryServiceName string
 54 | 	// Stdio indicates if Toolbox is listening via MCP stdio.
 55 | 	Stdio bool
 56 | 	// DisableReload indicates if the user has disabled dynamic reloading for Toolbox.
 57 | 	DisableReload bool
 58 | 	// UI indicates if Toolbox UI endpoints (/ui) are available
 59 | 	UI bool
 60 | }
 61 | 
 62 | type logFormat string
 63 | 
 64 | // String is used by both fmt.Print and by Cobra in help text
 65 | func (f *logFormat) String() string {
 66 | 	if string(*f) != "" {
 67 | 		return strings.ToLower(string(*f))
 68 | 	}
 69 | 	return "standard"
 70 | }
 71 | 
 72 | // validate logging format flag
 73 | func (f *logFormat) Set(v string) error {
 74 | 	switch strings.ToLower(v) {
 75 | 	case "standard", "json":
 76 | 		*f = logFormat(v)
 77 | 		return nil
 78 | 	default:
 79 | 		return fmt.Errorf(`log format must be one of "standard", or "json"`)
 80 | 	}
 81 | }
 82 | 
 83 | // Type is used in Cobra help text
 84 | func (f *logFormat) Type() string {
 85 | 	return "logFormat"
 86 | }
 87 | 
 88 | type StringLevel string
 89 | 
 90 | // String is used by both fmt.Print and by Cobra in help text
 91 | func (s *StringLevel) String() string {
 92 | 	if string(*s) != "" {
 93 | 		return strings.ToLower(string(*s))
 94 | 	}
 95 | 	return "info"
 96 | }
 97 | 
 98 | // validate log level flag
 99 | func (s *StringLevel) Set(v string) error {
100 | 	switch strings.ToLower(v) {
101 | 	case "debug", "info", "warn", "error":
102 | 		*s = StringLevel(v)
103 | 		return nil
104 | 	default:
105 | 		return fmt.Errorf(`log level must be one of "debug", "info", "warn", or "error"`)
106 | 	}
107 | }
108 | 
109 | // Type is used in Cobra help text
110 | func (s *StringLevel) Type() string {
111 | 	return "stringLevel"
112 | }
113 | 
114 | // SourceConfigs is a type used to allow unmarshal of the data source config map
115 | type SourceConfigs map[string]sources.SourceConfig
116 | 
117 | // validate interface
118 | var _ yaml.InterfaceUnmarshalerContext = &SourceConfigs{}
119 | 
120 | func (c *SourceConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
121 | 	*c = make(SourceConfigs)
122 | 	// Parse the 'kind' fields for each source
123 | 	var raw map[string]util.DelayedUnmarshaler
124 | 	if err := unmarshal(&raw); err != nil {
125 | 		return err
126 | 	}
127 | 
128 | 	for name, u := range raw {
129 | 		// Unmarshal to a general type that ensure it capture all fields
130 | 		var v map[string]any
131 | 		if err := u.Unmarshal(&v); err != nil {
132 | 			return fmt.Errorf("unable to unmarshal %q: %w", name, err)
133 | 		}
134 | 
135 | 		kind, ok := v["kind"]
136 | 		if !ok {
137 | 			return fmt.Errorf("missing 'kind' field for source %q", name)
138 | 		}
139 | 		kindStr, ok := kind.(string)
140 | 		if !ok {
141 | 			return fmt.Errorf("invalid 'kind' field for source %q (must be a string)", name)
142 | 		}
143 | 
144 | 		yamlDecoder, err := util.NewStrictDecoder(v)
145 | 		if err != nil {
146 | 			return fmt.Errorf("error creating YAML decoder for source %q: %w", name, err)
147 | 		}
148 | 
149 | 		sourceConfig, err := sources.DecodeConfig(ctx, kindStr, name, yamlDecoder)
150 | 		if err != nil {
151 | 			return err
152 | 		}
153 | 		(*c)[name] = sourceConfig
154 | 	}
155 | 	return nil
156 | }
157 | 
158 | // AuthServiceConfigs is a type used to allow unmarshal of the data authService config map
159 | type AuthServiceConfigs map[string]auth.AuthServiceConfig
160 | 
161 | // validate interface
162 | var _ yaml.InterfaceUnmarshalerContext = &AuthServiceConfigs{}
163 | 
164 | func (c *AuthServiceConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
165 | 	*c = make(AuthServiceConfigs)
166 | 	// Parse the 'kind' fields for each authService
167 | 	var raw map[string]util.DelayedUnmarshaler
168 | 	if err := unmarshal(&raw); err != nil {
169 | 		return err
170 | 	}
171 | 
172 | 	for name, u := range raw {
173 | 		var v map[string]any
174 | 		if err := u.Unmarshal(&v); err != nil {
175 | 			return fmt.Errorf("unable to unmarshal %q: %w", name, err)
176 | 		}
177 | 
178 | 		kind, ok := v["kind"]
179 | 		if !ok {
180 | 			return fmt.Errorf("missing 'kind' field for %q", name)
181 | 		}
182 | 
183 | 		dec, err := util.NewStrictDecoder(v)
184 | 		if err != nil {
185 | 			return fmt.Errorf("error creating decoder: %w", err)
186 | 		}
187 | 		switch kind {
188 | 		case google.AuthServiceKind:
189 | 			actual := google.Config{Name: name}
190 | 			if err := dec.DecodeContext(ctx, &actual); err != nil {
191 | 				return fmt.Errorf("unable to parse as %q: %w", kind, err)
192 | 			}
193 | 			(*c)[name] = actual
194 | 		default:
195 | 			return fmt.Errorf("%q is not a valid kind of auth source", kind)
196 | 		}
197 | 	}
198 | 	return nil
199 | }
200 | 
201 | // ToolConfigs is a type used to allow unmarshal of the tool configs
202 | type ToolConfigs map[string]tools.ToolConfig
203 | 
204 | // validate interface
205 | var _ yaml.InterfaceUnmarshalerContext = &ToolConfigs{}
206 | 
207 | func (c *ToolConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
208 | 	*c = make(ToolConfigs)
209 | 	// Parse the 'kind' fields for each source
210 | 	var raw map[string]util.DelayedUnmarshaler
211 | 	if err := unmarshal(&raw); err != nil {
212 | 		return err
213 | 	}
214 | 
215 | 	for name, u := range raw {
216 | 		var v map[string]any
217 | 		if err := u.Unmarshal(&v); err != nil {
218 | 			return fmt.Errorf("unable to unmarshal %q: %w", name, err)
219 | 		}
220 | 
221 | 		// `authRequired` and `useClientOAuth` cannot be specified together
222 | 		if v["authRequired"] != nil && v["useClientOAuth"] == true {
223 | 			return fmt.Errorf("`authRequired` and `useClientOAuth` are mutually exclusive. Choose only one authentication method")
224 | 		}
225 | 
226 | 		// Make `authRequired` an empty list instead of nil for Tool manifest
227 | 		if v["authRequired"] == nil {
228 | 			v["authRequired"] = []string{}
229 | 		}
230 | 
231 | 		kindVal, ok := v["kind"]
232 | 		if !ok {
233 | 			return fmt.Errorf("missing 'kind' field for tool %q", name)
234 | 		}
235 | 		kindStr, ok := kindVal.(string)
236 | 		if !ok {
237 | 			return fmt.Errorf("invalid 'kind' field for tool %q (must be a string)", name)
238 | 		}
239 | 
240 | 		yamlDecoder, err := util.NewStrictDecoder(v)
241 | 		if err != nil {
242 | 			return fmt.Errorf("error creating YAML decoder for tool %q: %w", name, err)
243 | 		}
244 | 
245 | 		toolCfg, err := tools.DecodeConfig(ctx, kindStr, name, yamlDecoder)
246 | 		if err != nil {
247 | 			return err
248 | 		}
249 | 		(*c)[name] = toolCfg
250 | 	}
251 | 	return nil
252 | }
253 | 
254 | // ToolConfigs is a type used to allow unmarshal of the toolset configs
255 | type ToolsetConfigs map[string]tools.ToolsetConfig
256 | 
257 | // validate interface
258 | var _ yaml.InterfaceUnmarshalerContext = &ToolsetConfigs{}
259 | 
260 | func (c *ToolsetConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
261 | 	*c = make(ToolsetConfigs)
262 | 
263 | 	var raw map[string][]string
264 | 	if err := unmarshal(&raw); err != nil {
265 | 		return err
266 | 	}
267 | 
268 | 	for name, toolList := range raw {
269 | 		(*c)[name] = tools.ToolsetConfig{Name: name, ToolNames: toolList}
270 | 	}
271 | 	return nil
272 | }
273 | 
```

--------------------------------------------------------------------------------
/internal/tools/sqlite/sqliteexecutesql/sqliteexecutesql_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 sqliteexecutesql_test
 16 | 
 17 | import (
 18 | 	"context"
 19 | 	"database/sql"
 20 | 	"reflect"
 21 | 	"testing"
 22 | 
 23 | 	yaml "github.com/goccy/go-yaml"
 24 | 	"github.com/google/go-cmp/cmp"
 25 | 	"github.com/googleapis/genai-toolbox/internal/server"
 26 | 	"github.com/googleapis/genai-toolbox/internal/testutils"
 27 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 28 | 	"github.com/googleapis/genai-toolbox/internal/tools/sqlite/sqliteexecutesql"
 29 | 	_ "modernc.org/sqlite"
 30 | )
 31 | 
 32 | func TestParseFromYamlExecuteSql(t *testing.T) {
 33 | 	ctx, err := testutils.ContextWithNewLogger()
 34 | 	if err != nil {
 35 | 		t.Fatalf("unexpected error: %s", err)
 36 | 	}
 37 | 	tcs := []struct {
 38 | 		desc string
 39 | 		in   string
 40 | 		want server.ToolConfigs
 41 | 	}{
 42 | 		{
 43 | 			desc: "basic example",
 44 | 			in: `
 45 | 			tools:
 46 | 				example_tool:
 47 | 					kind: sqlite-execute-sql
 48 | 					source: my-instance
 49 | 					description: some description
 50 | 					authRequired:
 51 | 						- my-google-auth-service
 52 | 						- other-auth-service
 53 | 			`,
 54 | 			want: server.ToolConfigs{
 55 | 				"example_tool": sqliteexecutesql.Config{
 56 | 					Name:         "example_tool",
 57 | 					Kind:         "sqlite-execute-sql",
 58 | 					Source:       "my-instance",
 59 | 					Description:  "some description",
 60 | 					AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
 61 | 				},
 62 | 			},
 63 | 		},
 64 | 	}
 65 | 	for _, tc := range tcs {
 66 | 		t.Run(tc.desc, func(t *testing.T) {
 67 | 			got := struct {
 68 | 				Tools server.ToolConfigs `yaml:"tools"`
 69 | 			}{}
 70 | 			// Parse contents
 71 | 			err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
 72 | 			if err != nil {
 73 | 				t.Fatalf("unable to unmarshal: %s", err)
 74 | 			}
 75 | 			if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
 76 | 				t.Fatalf("incorrect parse: diff %v", diff)
 77 | 			}
 78 | 		})
 79 | 	}
 80 | 
 81 | }
 82 | 
 83 | func setupTestDB(t *testing.T) *sql.DB {
 84 | 	db, err := sql.Open("sqlite", ":memory:")
 85 | 	if err != nil {
 86 | 		t.Fatalf("Failed to open in-memory database: %v", err)
 87 | 	}
 88 | 	return db
 89 | }
 90 | 
 91 | func TestTool_Invoke(t *testing.T) {
 92 | 	ctx, err := testutils.ContextWithNewLogger()
 93 | 	if err != nil {
 94 | 		t.Fatalf("unexpected error: %s", err)
 95 | 	}
 96 | 
 97 | 	type fields struct {
 98 | 		Name         string
 99 | 		Kind         string
100 | 		AuthRequired []string
101 | 		Parameters   tools.Parameters
102 | 		DB           *sql.DB
103 | 	}
104 | 	type args struct {
105 | 		ctx         context.Context
106 | 		params      tools.ParamValues
107 | 		accessToken tools.AccessToken
108 | 	}
109 | 	tests := []struct {
110 | 		name    string
111 | 		fields  fields
112 | 		args    args
113 | 		want    any
114 | 		wantErr bool
115 | 	}{
116 | 		{
117 | 			name: "create table",
118 | 			fields: fields{
119 | 				DB: setupTestDB(t),
120 | 			},
121 | 			args: args{
122 | 				ctx: ctx,
123 | 				params: []tools.ParamValue{
124 | 					{Name: "sql", Value: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"},
125 | 				},
126 | 			},
127 | 			want:    nil,
128 | 			wantErr: false,
129 | 		},
130 | 		{
131 | 			name: "insert data",
132 | 			fields: fields{
133 | 				DB: setupTestDB(t),
134 | 			},
135 | 			args: args{
136 | 				ctx: ctx,
137 | 				params: []tools.ParamValue{
138 | 					{Name: "sql", Value: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER); INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30), (2, 'Bob', 25)"},
139 | 				},
140 | 			},
141 | 			want:    nil,
142 | 			wantErr: false,
143 | 		},
144 | 		{
145 | 			name: "select data",
146 | 			fields: fields{
147 | 				DB: func() *sql.DB {
148 | 					db := setupTestDB(t)
149 | 					if _, err := db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER); INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30), (2, 'Bob', 25)"); err != nil {
150 | 						t.Fatalf("Failed to set up database for select: %v", err)
151 | 					}
152 | 					return db
153 | 				}(),
154 | 			},
155 | 			args: args{
156 | 				ctx: ctx,
157 | 				params: []tools.ParamValue{
158 | 					{Name: "sql", Value: "SELECT * FROM users"},
159 | 				},
160 | 			},
161 | 			want: []any{
162 | 				map[string]any{"id": int64(1), "name": "Alice", "age": int64(30)},
163 | 				map[string]any{"id": int64(2), "name": "Bob", "age": int64(25)},
164 | 			},
165 | 			wantErr: false,
166 | 		},
167 | 		{
168 | 			name: "drop table",
169 | 			fields: fields{
170 | 				DB: func() *sql.DB {
171 | 					db := setupTestDB(t)
172 | 					if _, err := db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"); err != nil {
173 | 						t.Fatalf("Failed to set up database for drop: %v", err)
174 | 					}
175 | 					return db
176 | 				}(),
177 | 			},
178 | 			args: args{
179 | 				ctx: ctx,
180 | 				params: []tools.ParamValue{
181 | 					{Name: "sql", Value: "DROP TABLE users"},
182 | 				},
183 | 			},
184 | 			want:    nil,
185 | 			wantErr: false,
186 | 		},
187 | 		{
188 | 			name: "invalid sql",
189 | 			fields: fields{
190 | 				DB: setupTestDB(t),
191 | 			},
192 | 			args: args{
193 | 				ctx: ctx,
194 | 				params: []tools.ParamValue{
195 | 					{Name: "sql", Value: "SELECT * FROM non_existent_table"},
196 | 				},
197 | 			},
198 | 			want:    nil,
199 | 			wantErr: true,
200 | 		},
201 | 		{
202 | 			name: "empty sql",
203 | 			fields: fields{
204 | 				DB: setupTestDB(t),
205 | 			},
206 | 			args: args{
207 | 				ctx: ctx,
208 | 				params: []tools.ParamValue{
209 | 					{Name: "sql", Value: ""},
210 | 				},
211 | 			},
212 | 			want:    nil,
213 | 			wantErr: true,
214 | 		},
215 | 		{
216 | 			name: "data types",
217 | 			fields: fields{
218 | 				DB: func() *sql.DB {
219 | 					db := setupTestDB(t)
220 | 					if _, err := db.Exec("CREATE TABLE data_types (id INTEGER PRIMARY KEY, null_col TEXT, blob_col BLOB)"); err != nil {
221 | 						t.Fatalf("Failed to set up database for data types: %v", err)
222 | 					}
223 | 					if _, err := db.Exec("INSERT INTO data_types (id, null_col, blob_col) VALUES (1, NULL, ?)", []byte{1, 2, 3}); err != nil {
224 | 						t.Fatalf("Failed to insert data for data types: %v", err)
225 | 					}
226 | 					return db
227 | 				}(),
228 | 			},
229 | 			args: args{
230 | 				ctx: ctx,
231 | 				params: []tools.ParamValue{
232 | 					{Name: "sql", Value: "SELECT * FROM data_types"},
233 | 				},
234 | 			},
235 | 			want: []any{
236 | 				map[string]any{"id": int64(1), "null_col": nil, "blob_col": []byte{1, 2, 3}},
237 | 			},
238 | 			wantErr: false,
239 | 		},
240 | 		{
241 | 			name: "join operation",
242 | 			fields: fields{
243 | 				DB: func() *sql.DB {
244 | 					db := setupTestDB(t)
245 | 					if _, err := db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"); err != nil {
246 | 						t.Fatalf("Failed to set up database for join: %v", err)
247 | 					}
248 | 					if _, err := db.Exec("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30), (2, 'Bob', 25)"); err != nil {
249 | 						t.Fatalf("Failed to insert data for join: %v", err)
250 | 					}
251 | 					if _, err := db.Exec("CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, item TEXT)"); err != nil {
252 | 						t.Fatalf("Failed to set up database for join: %v", err)
253 | 					}
254 | 					if _, err := db.Exec("INSERT INTO orders (id, user_id, item) VALUES (1, 1, 'Laptop'), (2, 2, 'Keyboard')"); err != nil {
255 | 						t.Fatalf("Failed to insert data for join: %v", err)
256 | 					}
257 | 					return db
258 | 				}(),
259 | 			},
260 | 			args: args{
261 | 				ctx: ctx,
262 | 				params: []tools.ParamValue{
263 | 					{Name: "sql", Value: "SELECT u.name, o.item FROM users u JOIN orders o ON u.id = o.user_id"},
264 | 				},
265 | 			},
266 | 			want: []any{
267 | 				map[string]any{"name": "Alice", "item": "Laptop"},
268 | 				map[string]any{"name": "Bob", "item": "Keyboard"},
269 | 			},
270 | 			wantErr: false,
271 | 		},
272 | 	}
273 | 	for _, tt := range tests {
274 | 		t.Run(tt.name, func(t *testing.T) {
275 | 			tr := &sqliteexecutesql.Tool{
276 | 				Name:         tt.fields.Name,
277 | 				Kind:         tt.fields.Kind,
278 | 				AuthRequired: tt.fields.AuthRequired,
279 | 				Parameters:   tt.fields.Parameters,
280 | 				DB:           tt.fields.DB,
281 | 			}
282 | 			got, err := tr.Invoke(tt.args.ctx, tt.args.params, tt.args.accessToken)
283 | 			if (err != nil) != tt.wantErr {
284 | 				t.Errorf("Tool.Invoke() error = %v, wantErr %v", err, tt.wantErr)
285 | 				return
286 | 			}
287 | 			isEqual := false
288 | 			if got != nil && len(got.([]any)) == 0 && len(tt.want.([]any)) == 0 {
289 | 				isEqual = true // Special case for empty slices, since DeepEqual returns false
290 | 			} else {
291 | 				isEqual = reflect.DeepEqual(got, tt.want)
292 | 			}
293 | 
294 | 			if !isEqual {
295 | 				t.Errorf("Tool.Invoke() = %v, want %v", got, tt.want)
296 | 			}
297 | 		})
298 | 	}
299 | }
300 | 
```

--------------------------------------------------------------------------------
/docs/en/how-to/deploy_toolbox.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: "Deploy to Cloud Run"
  3 | type: docs
  4 | weight: 3
  5 | description: >
  6 |   How to set up and configure Toolbox to run on Cloud Run.
  7 | ---
  8 | 
  9 | 
 10 | ## Before you begin
 11 | 
 12 | 1. [Install](https://cloud.google.com/sdk/docs/install) the Google Cloud CLI.
 13 | 
 14 | 1. Set the PROJECT_ID environment variable:
 15 | 
 16 |     ```bash
 17 |     export PROJECT_ID="my-project-id"
 18 |     ```
 19 | 
 20 | 1. Initialize gcloud CLI:
 21 | 
 22 |     ```bash
 23 |     gcloud init
 24 |     gcloud config set project $PROJECT_ID
 25 |     ```
 26 | 
 27 | 1. Make sure you've set up and initialized your database.
 28 | 
 29 | 1. You must have the following APIs enabled:
 30 | 
 31 |     ```bash
 32 |     gcloud services enable run.googleapis.com \
 33 |                            cloudbuild.googleapis.com \
 34 |                            artifactregistry.googleapis.com \
 35 |                            iam.googleapis.com \
 36 |                            secretmanager.googleapis.com
 37 | 
 38 |     ```
 39 | 
 40 | 1. To create an IAM account, you must have the following IAM permissions (or
 41 |    roles):
 42 |     - Create Service Account role (roles/iam.serviceAccountCreator)
 43 | 
 44 | 1. To create a secret, you must have the following roles:
 45 |     - Secret Manager Admin role (roles/secretmanager.admin)
 46 | 
 47 | 1. To deploy to Cloud Run, you must have the following set of roles:
 48 |     - Cloud Run Developer (roles/run.developer)
 49 |     - Service Account User role (roles/iam.serviceAccountUser)
 50 | 
 51 | {{< notice note >}}
 52 | If you are using sources that require VPC-access (such as
 53 | AlloyDB or Cloud SQL over private IP), make sure your Cloud Run service and the
 54 | database are in the same VPC network.
 55 | {{< /notice >}}
 56 | 
 57 | ## Create a service account
 58 | 
 59 | 1. Create a backend service account if you don't already have one:
 60 | 
 61 |     ```bash
 62 |     gcloud iam service-accounts create toolbox-identity
 63 |     ```
 64 | 
 65 | 1. Grant permissions to use secret manager:
 66 | 
 67 |     ```bash
 68 |     gcloud projects add-iam-policy-binding $PROJECT_ID \
 69 |         --member serviceAccount:toolbox-identity@$PROJECT_ID.iam.gserviceaccount.com \
 70 |         --role roles/secretmanager.secretAccessor
 71 |     ```
 72 | 
 73 | 1. Grant additional permissions to the service account that are specific to the
 74 |    source, e.g.:
 75 |     - [AlloyDB for PostgreSQL](../resources/sources/alloydb-pg.md#iam-permissions)
 76 |     - [Cloud SQL for PostgreSQL](../resources/sources/cloud-sql-pg.md#iam-permissions)
 77 | 
 78 | ## Configure `tools.yaml` file
 79 | 
 80 | Create a `tools.yaml` file that contains your configuration for Toolbox. For
 81 | details, see the
 82 | [configuration](../resources/sources/)
 83 | section.
 84 | 
 85 | ## Deploy to Cloud Run
 86 | 
 87 | 1. Upload `tools.yaml` as a secret:
 88 | 
 89 |     ```bash
 90 |     gcloud secrets create tools --data-file=tools.yaml
 91 |     ```
 92 | 
 93 |     If you already have a secret and want to update the secret version, execute
 94 |     the following:
 95 | 
 96 |     ```bash
 97 |     gcloud secrets versions add tools --data-file=tools.yaml
 98 |     ```
 99 | 
100 | 1. Set an environment variable to the container image that you want to use for
101 |    cloud run:
102 | 
103 |     ```bash
104 |     export IMAGE=us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest
105 |     ```
106 | 
107 | {{< notice note >}}  
108 | **The `$PORT` Environment Variable**  
109 | Google Cloud Run dictates the port your application must listen on by setting
110 | the `$PORT` environment variable inside your container. This value defaults to
111 | **8080**. Your application's `--port` argument **must** be set to listen on this
112 | port. If there is a mismatch, the container will fail to start and the
113 | deployment will time out.  
114 | {{< /notice >}}
115 | 
116 | 1. Deploy Toolbox to Cloud Run using the following command:
117 | 
118 |     ```bash
119 |     gcloud run deploy toolbox \
120 |         --image $IMAGE \
121 |         --service-account toolbox-identity \
122 |         --region us-central1 \
123 |         --set-secrets "/app/tools.yaml=tools:latest" \
124 |         --args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080"
125 |         # --allow-unauthenticated # https://cloud.google.com/run/docs/authenticating/public#gcloud
126 |     ```
127 | 
128 |     If you are using a VPC network, use the command below:
129 | 
130 |     ```bash
131 |     gcloud run deploy toolbox \
132 |         --image $IMAGE \
133 |         --service-account toolbox-identity \
134 |         --region us-central1 \
135 |         --set-secrets "/app/tools.yaml=tools:latest" \
136 |         --args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080" \
137 |         # TODO(dev): update the following to match your VPC if necessary
138 |         --network default \
139 |         --subnet default
140 |         # --allow-unauthenticated # https://cloud.google.com/run/docs/authenticating/public#gcloud
141 |     ```
142 | 
143 | ## Connecting with Toolbox Client SDK
144 | 
145 | You can connect to Toolbox Cloud Run instances directly through the SDK.
146 | 
147 | 1. [Set up `Cloud Run Invoker` role
148 |    access](https://cloud.google.com/run/docs/securing/managing-access#service-add-principals)
149 |    to your Cloud Run service.
150 | 
151 | 1. (Only for local runs) Set up [Application Default
152 |    Credentials](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)
153 |    for the principal you set up the `Cloud Run Invoker` role access to.
154 | 
155 | 1. Run the following to retrieve a non-deterministic URL for the cloud run service:
156 | 
157 |     ```bash
158 |     gcloud run services describe toolbox --format 'value(status.url)'
159 |     ```
160 | 
161 | 1. Import and initialize the toolbox client with the URL retrieved above:
162 | 
163 |     {{< tabpane persist=header >}}
164 | {{< tab header="Python" lang="python" >}}
165 | from toolbox_core import ToolboxClient, auth_methods
166 | 
167 | # Replace with the Cloud Run service URL generated in the previous step.
168 | URL = "https://cloud-run-url.app"
169 | 
170 | auth_token_provider = auth_methods.aget_google_id_token(URL) # can also use sync method
171 | 
172 | async with ToolboxClient(
173 |     URL,
174 |     client_headers={"Authorization": auth_token_provider},
175 | ) as toolbox:
176 | {{< /tab >}}
177 | {{< tab header="Javascript" lang="javascript" >}}
178 | import { ToolboxClient } from '@toolbox-sdk/core';
179 | import {getGoogleIdToken} from '@toolbox-sdk/core/auth'
180 | 
181 | // Replace with the Cloud Run service URL generated in the previous step.
182 | const URL = 'http://127.0.0.1:5000';
183 | const authTokenProvider = () => getGoogleIdToken(URL);
184 | 
185 | const client = new ToolboxClient(URL, null, {"Authorization": authTokenProvider});
186 | {{< /tab >}}
187 | {{< tab header="Go" lang="go" >}}
188 | import "github.com/googleapis/mcp-toolbox-sdk-go/core"
189 | 
190 | func main() {
191 |     // Replace with the Cloud Run service URL generated in the previous step.
192 |     URL := "http://127.0.0.1:5000"
193 |     auth_token_provider, err := core.GetGoogleIDToken(ctx, URL)
194 |     if err != nil {
195 |         log.Fatalf("Failed to fetch token %v", err)
196 |     }
197 |     toolboxClient, err := core.NewToolboxClient(
198 |         URL,
199 |         core.WithClientHeaderString("Authorization", auth_token_provider))
200 |     if err != nil {
201 |         log.Fatalf("Failed to create Toolbox client: %v", err)
202 |     }
203 | }
204 | {{< /tab >}}
205 | {{< /tabpane >}}
206 | 
207 | 
208 | Now, you can use this client to connect to the deployed Cloud Run instance!
209 | 
210 | ## Troubleshooting
211 | 
212 | {{< notice note >}}  
213 | For any deployment or runtime error, the best first step is to check the logs
214 | for your service in the Google Cloud Console's Cloud Run section. They often
215 | contain the specific error message needed to diagnose the problem.
216 | {{< /notice >}}
217 | 
218 | * **Deployment Fails with "Container failed to start":** This is almost always
219 |     caused by a port mismatch. Ensure your container's `--port` argument is set to
220 |     `8080` to match the `$PORT` environment variable provided by Cloud Run.
221 | 
222 | * **Client Receives Permission Denied Error (401 or 403):** If your client
223 |   application (e.g., your local SDK) gets a `401 Unauthorized` or `403
224 |   Forbidden` error when trying to call your Cloud Run service, it means the
225 |   client is not properly authenticated as an invoker.
226 |     * Ensure the user or service account calling the service has the **Cloud Run
227 |       Invoker** (`roles/run.invoker`) IAM role.
228 |     * If running locally, make sure your Application Default Credentials are set
229 |       up correctly by running `gcloud auth application-default login`.
230 | 
231 | * **Service Fails to Access Secrets (in logs):** If your application starts but
232 |   the logs show errors like "permission denied" when trying to access Secret
233 |   Manager, it means the Toolbox service account is missing permissions.
234 |     * Ensure the `toolbox-identity` service account has the **Secret Manager
235 |       Secret Accessor** (`roles/secretmanager.secretAccessor`) IAM role.
```
Page 23/48FirstPrevNextLast