This is page 39 of 59. Use http://codebase.md/googleapis/genai-toolbox?format=md&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
│       │       │   ├── adkgo
│       │       │   │   ├── go.mod
│       │       │   │   ├── go.sum
│       │       │   │   └── 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-healthcare.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
│       │   │   ├── elasticsearch.md
│       │   │   ├── firebird.md
│       │   │   ├── firestore.md
│       │   │   ├── http.md
│       │   │   ├── looker.md
│       │   │   ├── mindsdb.md
│       │   │   ├── mongodb.md
│       │   │   ├── mssql.md
│       │   │   ├── mysql.md
│       │   │   ├── neo4j.md
│       │   │   ├── oceanbase.md
│       │   │   ├── oracle.md
│       │   │   ├── postgres.md
│       │   │   ├── redis.md
│       │   │   ├── serverless-spark.md
│       │   │   ├── singlestore.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
│       │       ├── cloudhealthcare
│       │       │   ├── _index.md
│       │       │   ├── cloud-healthcare-fhir-fetch-page.md
│       │       │   ├── cloud-healthcare-fhir-patient-everything.md
│       │       │   ├── cloud-healthcare-fhir-patient-search.md
│       │       │   ├── cloud-healthcare-get-dataset.md
│       │       │   ├── cloud-healthcare-get-dicom-store-metrics.md
│       │       │   ├── cloud-healthcare-get-dicom-store.md
│       │       │   ├── cloud-healthcare-get-fhir-resource.md
│       │       │   ├── cloud-healthcare-get-fhir-store-metrics.md
│       │       │   ├── cloud-healthcare-get-fhir-store.md
│       │       │   ├── cloud-healthcare-list-dicom-stores.md
│       │       │   ├── cloud-healthcare-list-fhir-stores.md
│       │       │   ├── cloud-healthcare-retrieve-rendered-dicom-instance.md
│       │       │   ├── cloud-healthcare-search-dicom-instances.md
│       │       │   ├── cloud-healthcare-search-dicom-series.md
│       │       │   └── cloud-healthcare-search-dicom-studies.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
│       │       ├── elasticsearch
│       │       │   ├── _index.md
│       │       │   └── elasticsearch-esql.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-connection-databases.md
│       │       │   ├── looker-get-connection-schemas.md
│       │       │   ├── looker-get-connection-table-columns.md
│       │       │   ├── looker-get-connection-tables.md
│       │       │   ├── looker-get-connections.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-dashboard.md
│       │       │   ├── looker-run-look.md
│       │       │   └── looker-update-project-file.md
│       │       ├── mindsdb
│       │       │   ├── _index.md
│       │       │   ├── mindsdb-execute-sql.md
│       │       │   └── mindsdb-sql.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-schemas.md
│       │       │   ├── postgres-list-tables.md
│       │       │   ├── postgres-list-views.md
│       │       │   └── postgres-sql.md
│       │       ├── redis
│       │       │   ├── _index.md
│       │       │   └── redis.md
│       │       ├── serverless-spark
│       │       │   ├── _index.md
│       │       │   ├── serverless-spark-cancel-batch.md
│       │       │   ├── serverless-spark-get-batch.md
│       │       │   └── serverless-spark-list-batches.md
│       │       ├── singlestore
│       │       │   ├── _index.md
│       │       │   ├── singlestore-execute-sql.md
│       │       │   └── singlestore-sql.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-healthcare.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
│   │       ├── elasticsearch.yaml
│   │       ├── firestore.yaml
│   │       ├── looker-conversational-analytics.yaml
│   │       ├── looker.yaml
│   │       ├── mindsdb.yaml
│   │       ├── mssql.yaml
│   │       ├── mysql.yaml
│   │       ├── neo4j.yaml
│   │       ├── oceanbase.yaml
│   │       ├── postgres.yaml
│   │       ├── serverless-spark.yaml
│   │       ├── singlestore.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
│   │   │   └── cache.go
│   │   ├── bigtable
│   │   │   ├── bigtable_test.go
│   │   │   └── bigtable.go
│   │   ├── cassandra
│   │   │   ├── cassandra_test.go
│   │   │   └── cassandra.go
│   │   ├── clickhouse
│   │   │   ├── clickhouse_test.go
│   │   │   └── clickhouse.go
│   │   ├── cloudhealthcare
│   │   │   ├── cloud_healthcare_test.go
│   │   │   └── cloud_healthcare.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
│   │   ├── elasticsearch
│   │   │   ├── elasticsearch_test.go
│   │   │   └── elasticsearch.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
│   │   ├── mindsdb
│   │   │   ├── mindsdb_test.go
│   │   │   └── mindsdb.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
│   │   ├── serverlessspark
│   │   │   ├── serverlessspark_test.go
│   │   │   └── serverlessspark.go
│   │   ├── singlestore
│   │   │   ├── singlestore_test.go
│   │   │   └── singlestore.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
│   │   ├── cloudhealthcare
│   │   │   ├── cloudhealthcarefhirfetchpage
│   │   │   │   ├── cloudhealthcarefhirfetchpage_test.go
│   │   │   │   └── cloudhealthcarefhirfetchpage.go
│   │   │   ├── cloudhealthcarefhirpatienteverything
│   │   │   │   ├── cloudhealthcarefhirpatienteverything_test.go
│   │   │   │   └── cloudhealthcarefhirpatienteverything.go
│   │   │   ├── cloudhealthcarefhirpatientsearch
│   │   │   │   ├── cloudhealthcarefhirpatientsearch_test.go
│   │   │   │   └── cloudhealthcarefhirpatientsearch.go
│   │   │   ├── cloudhealthcaregetdataset
│   │   │   │   ├── cloudhealthcaregetdataset_test.go
│   │   │   │   └── cloudhealthcaregetdataset.go
│   │   │   ├── cloudhealthcaregetdicomstore
│   │   │   │   ├── cloudhealthcaregetdicomstore_test.go
│   │   │   │   └── cloudhealthcaregetdicomstore.go
│   │   │   ├── cloudhealthcaregetdicomstoremetrics
│   │   │   │   ├── cloudhealthcaregetdicomstoremetrics_test.go
│   │   │   │   └── cloudhealthcaregetdicomstoremetrics.go
│   │   │   ├── cloudhealthcaregetfhirresource
│   │   │   │   ├── cloudhealthcaregetfhirresource_test.go
│   │   │   │   └── cloudhealthcaregetfhirresource.go
│   │   │   ├── cloudhealthcaregetfhirstore
│   │   │   │   ├── cloudhealthcaregetfhirstore_test.go
│   │   │   │   └── cloudhealthcaregetfhirstore.go
│   │   │   ├── cloudhealthcaregetfhirstoremetrics
│   │   │   │   ├── cloudhealthcaregetfhirstoremetrics_test.go
│   │   │   │   └── cloudhealthcaregetfhirstoremetrics.go
│   │   │   ├── cloudhealthcarelistdicomstores
│   │   │   │   ├── cloudhealthcarelistdicomstores_test.go
│   │   │   │   └── cloudhealthcarelistdicomstores.go
│   │   │   ├── cloudhealthcarelistfhirstores
│   │   │   │   ├── cloudhealthcarelistfhirstores_test.go
│   │   │   │   └── cloudhealthcarelistfhirstores.go
│   │   │   ├── cloudhealthcareretrieverendereddicominstance
│   │   │   │   ├── cloudhealthcareretrieverendereddicominstance_test.go
│   │   │   │   └── cloudhealthcareretrieverendereddicominstance.go
│   │   │   ├── cloudhealthcaresearchdicominstances
│   │   │   │   ├── cloudhealthcaresearchdicominstances_test.go
│   │   │   │   └── cloudhealthcaresearchdicominstances.go
│   │   │   ├── cloudhealthcaresearchdicomseries
│   │   │   │   ├── cloudhealthcaresearchdicomseries_test.go
│   │   │   │   └── cloudhealthcaresearchdicomseries.go
│   │   │   ├── cloudhealthcaresearchdicomstudies
│   │   │   │   ├── cloudhealthcaresearchdicomstudies_test.go
│   │   │   │   └── cloudhealthcaresearchdicomstudies.go
│   │   │   └── common
│   │   │       └── util.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
│   │   ├── elasticsearch
│   │   │   └── elasticsearchesql
│   │   │       ├── elasticsearchesql_test.go
│   │   │       └── elasticsearchesql.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
│   │   │   ├── lookergetconnectiondatabases
│   │   │   │   ├── lookergetconnectiondatabases_test.go
│   │   │   │   └── lookergetconnectiondatabases.go
│   │   │   ├── lookergetconnections
│   │   │   │   ├── lookergetconnections_test.go
│   │   │   │   └── lookergetconnections.go
│   │   │   ├── lookergetconnectionschemas
│   │   │   │   ├── lookergetconnectionschemas_test.go
│   │   │   │   └── lookergetconnectionschemas.go
│   │   │   ├── lookergetconnectiontablecolumns
│   │   │   │   ├── lookergetconnectiontablecolumns_test.go
│   │   │   │   └── lookergetconnectiontablecolumns.go
│   │   │   ├── lookergetconnectiontables
│   │   │   │   ├── lookergetconnectiontables_test.go
│   │   │   │   └── lookergetconnectiontables.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
│   │   │   ├── lookerrundashboard
│   │   │   │   ├── lookerrundashboard_test.go
│   │   │   │   └── lookerrundashboard.go
│   │   │   ├── lookerrunlook
│   │   │   │   ├── lookerrunlook_test.go
│   │   │   │   └── lookerrunlook.go
│   │   │   └── lookerupdateprojectfile
│   │   │       ├── lookerupdateprojectfile_test.go
│   │   │       └── lookerupdateprojectfile.go
│   │   ├── mindsdb
│   │   │   ├── mindsdbexecutesql
│   │   │   │   ├── mindsdbexecutesql_test.go
│   │   │   │   └── mindsdbexecutesql.go
│   │   │   └── mindsdbsql
│   │   │       ├── mindsdbsql_test.go
│   │   │       └── mindsdbsql.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
│   │   │   ├── postgreslistschemas
│   │   │   │   ├── postgreslistschemas_test.go
│   │   │   │   └── postgreslistschemas.go
│   │   │   ├── postgreslisttables
│   │   │   │   ├── postgreslisttables_test.go
│   │   │   │   └── postgreslisttables.go
│   │   │   ├── postgreslistviews
│   │   │   │   ├── postgreslistviews_test.go
│   │   │   │   └── postgreslistviews.go
│   │   │   └── postgressql
│   │   │       ├── postgressql_test.go
│   │   │       └── postgressql.go
│   │   ├── redis
│   │   │   ├── redis_test.go
│   │   │   └── redis.go
│   │   ├── serverlessspark
│   │   │   ├── serverlesssparkcancelbatch
│   │   │   │   ├── serverlesssparkcancelbatch_test.go
│   │   │   │   └── serverlesssparkcancelbatch.go
│   │   │   ├── serverlesssparkgetbatch
│   │   │   │   ├── serverlesssparkgetbatch_test.go
│   │   │   │   └── serverlesssparkgetbatch.go
│   │   │   └── serverlesssparklistbatches
│   │   │       ├── serverlesssparklistbatches_test.go
│   │   │       └── serverlesssparklistbatches.go
│   │   ├── singlestore
│   │   │   ├── singlestoreexecutesql
│   │   │   │   ├── singlestoreexecutesql_test.go
│   │   │   │   └── singlestoreexecutesql.go
│   │   │   └── singlestoresql
│   │   │       ├── singlestoresql_test.go
│   │   │       └── singlestoresql.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
│       ├── orderedmap
│       │   ├── orderedmap_test.go
│       │   └── orderedmap.go
│       └── 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
    ├── cloudhealthcare
    │   └── cloud_healthcare_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
    ├── elasticsearch
    │   └── elasticsearch_integration_test.go
    ├── firebird
    │   └── firebird_integration_test.go
    ├── firestore
    │   └── firestore_integration_test.go
    ├── http
    │   └── http_integration_test.go
    ├── looker
    │   └── looker_integration_test.go
    ├── mindsdb
    │   └── mindsdb_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
    ├── serverlessspark
    │   └── serverless_spark_integration_test.go
    ├── singlestore
    │   └── singlestore_integration_test.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/mysql/mysqllisttables/mysqllisttables.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 mysqllisttables
 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 | )
 29 | 
 30 | const kind string = "mysql-list-tables"
 31 | 
 32 | const listTablesStatement = `
 33 |     SELECT
 34 |         T.TABLE_SCHEMA AS schema_name,
 35 |         T.TABLE_NAME AS object_name,
 36 |         CASE
 37 |             WHEN @output_format = 'simple' THEN
 38 |                 JSON_OBJECT('name', T.TABLE_NAME)
 39 |             ELSE
 40 |                 CONVERT(
 41 |                     JSON_OBJECT(
 42 |                         'schema_name', T.TABLE_SCHEMA,
 43 |                         'object_name', T.TABLE_NAME,
 44 |                         'object_type', 'TABLE',
 45 |                         'owner', (
 46 |                             SELECT
 47 |                                 IFNULL(U.GRANTEE, 'N/A')
 48 |                             FROM
 49 |                                 INFORMATION_SCHEMA.SCHEMA_PRIVILEGES U
 50 |                             WHERE
 51 |                                 U.TABLE_SCHEMA = T.TABLE_SCHEMA
 52 |                             LIMIT 1
 53 |                         ),
 54 |                         'comment', IFNULL(T.TABLE_COMMENT, ''),
 55 |                         'columns', (
 56 |                             SELECT
 57 |                                 IFNULL(
 58 |                                     JSON_ARRAYAGG(
 59 |                                         JSON_OBJECT(
 60 |                                             'column_name', C.COLUMN_NAME,
 61 |                                             'data_type', C.COLUMN_TYPE,
 62 |                                             'ordinal_position', C.ORDINAL_POSITION,
 63 |                                             'is_not_nullable', IF(C.IS_NULLABLE = 'NO', TRUE, FALSE),
 64 |                                             'column_default', C.COLUMN_DEFAULT,
 65 |                                             'column_comment', IFNULL(C.COLUMN_COMMENT, '')
 66 |                                         )
 67 |                                     ),
 68 |                                     JSON_ARRAY()
 69 |                                 )
 70 |                             FROM
 71 |                                 INFORMATION_SCHEMA.COLUMNS C
 72 |                             WHERE
 73 |                                 C.TABLE_SCHEMA = T.TABLE_SCHEMA AND C.TABLE_NAME = T.TABLE_NAME
 74 |                             ORDER BY C.ORDINAL_POSITION
 75 |                         ),
 76 |                         'constraints', (
 77 |                             SELECT
 78 |                                 IFNULL(
 79 |                                     JSON_ARRAYAGG(
 80 |                                         JSON_OBJECT(
 81 |                                             'constraint_name', TC.CONSTRAINT_NAME,
 82 |                                             'constraint_type',
 83 |                                             CASE TC.CONSTRAINT_TYPE
 84 |                                                 WHEN 'PRIMARY KEY' THEN 'PRIMARY KEY'
 85 |                                                 WHEN 'FOREIGN KEY' THEN 'FOREIGN KEY'
 86 |                                                 WHEN 'UNIQUE' THEN 'UNIQUE'
 87 |                                                 ELSE TC.CONSTRAINT_TYPE
 88 |                                             END,
 89 |                                             'constraint_definition', '',
 90 |                                             'constraint_columns', (
 91 |                                                 SELECT
 92 |                                                     IFNULL(JSON_ARRAYAGG(KCU.COLUMN_NAME), JSON_ARRAY())
 93 |                                                 FROM
 94 |                                                     INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU
 95 |                                                 WHERE
 96 |                                                     KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
 97 |                                                     AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
 98 |                                                     AND KCU.TABLE_NAME = TC.TABLE_NAME
 99 |                                                 ORDER BY KCU.ORDINAL_POSITION
100 |                                             ),
101 |                                             'foreign_key_referenced_table', IF(TC.CONSTRAINT_TYPE = 'FOREIGN KEY', RC.REFERENCED_TABLE_NAME, NULL),
102 |                                             'foreign_key_referenced_columns', IF(TC.CONSTRAINT_TYPE = 'FOREIGN KEY',
103 |                                                 (SELECT IFNULL(JSON_ARRAYAGG(FKCU.REFERENCED_COLUMN_NAME), JSON_ARRAY())
104 |                                                 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE FKCU
105 |                                                 WHERE FKCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
106 |                                                     AND FKCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
107 |                                                     AND FKCU.TABLE_NAME = TC.TABLE_NAME
108 |                                                     AND FKCU.REFERENCED_TABLE_NAME IS NOT NULL
109 |                                                 ORDER BY FKCU.ORDINAL_POSITION),
110 |                                                 NULL
111 |                                             )
112 |                                         )
113 |                                     ),
114 |                                     JSON_ARRAY()
115 |                                 )
116 |                             FROM
117 |                                 INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC
118 |                             LEFT JOIN
119 |                                 INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC
120 |                                 ON TC.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA
121 |                                 AND TC.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
122 |                                 AND TC.TABLE_NAME = RC.TABLE_NAME
123 |                             WHERE
124 |                                 TC.TABLE_SCHEMA = T.TABLE_SCHEMA AND TC.TABLE_NAME = T.TABLE_NAME
125 |                         ),
126 |                         'indexes', (
127 |                             SELECT
128 |                                 IFNULL(
129 |                                     JSON_ARRAYAGG(
130 |                                         JSON_OBJECT(
131 |                                             'index_name', IndexData.INDEX_NAME,
132 |                                             'is_unique', IF(IndexData.NON_UNIQUE = 0, TRUE, FALSE),
133 |                                             'is_primary', IF(IndexData.INDEX_NAME = 'PRIMARY', TRUE, FALSE),
134 |                                             'index_columns', IFNULL(IndexData.INDEX_COLUMNS_ARRAY, JSON_ARRAY())
135 |                                         )
136 |                                     ),
137 |                                     JSON_ARRAY()
138 |                                 )
139 |                             FROM (
140 |                                 SELECT
141 |                                     S.TABLE_SCHEMA,
142 |                                     S.TABLE_NAME,
143 |                                     S.INDEX_NAME,
144 |                                     MIN(S.NON_UNIQUE) AS NON_UNIQUE,
145 |                                     JSON_ARRAYAGG(S.COLUMN_NAME) AS INDEX_COLUMNS_ARRAY
146 |                                 FROM
147 |                                     INFORMATION_SCHEMA.STATISTICS S
148 |                                 GROUP BY
149 |                                     S.TABLE_SCHEMA, S.TABLE_NAME, S.INDEX_NAME
150 |                             ) AS IndexData
151 |                             WHERE IndexData.TABLE_SCHEMA = T.TABLE_SCHEMA AND IndexData.TABLE_NAME = T.TABLE_NAME
152 |                             ORDER BY IndexData.INDEX_NAME
153 |                         ),
154 |                         'triggers', (
155 |                             SELECT
156 |                                 IFNULL(
157 |                                     JSON_ARRAYAGG(
158 |                                         JSON_OBJECT(
159 |                                             'trigger_name', TR.TRIGGER_NAME,
160 |                                             'trigger_definition', TR.ACTION_STATEMENT
161 |                                         )
162 |                                     ),
163 |                                     JSON_ARRAY()
164 |                                 )
165 |                             FROM
166 |                                 INFORMATION_SCHEMA.TRIGGERS TR
167 |                             WHERE
168 |                                 TR.EVENT_OBJECT_SCHEMA = T.TABLE_SCHEMA AND TR.EVENT_OBJECT_TABLE = T.TABLE_NAME
169 |                             ORDER BY TR.TRIGGER_NAME
170 |                         )
171 |                     )
172 |                 USING utf8mb4)
173 |         END AS object_details
174 |     FROM
175 |         INFORMATION_SCHEMA.TABLES T
176 |     CROSS JOIN (SELECT @table_names := ?, @output_format := ?) AS variables
177 |     WHERE
178 |         T.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
179 |         AND (NULLIF(TRIM(@table_names), '') IS NULL OR FIND_IN_SET(T.TABLE_NAME, @table_names))
180 |         AND T.TABLE_TYPE = 'BASE TABLE'
181 |     ORDER BY
182 |         T.TABLE_SCHEMA, T.TABLE_NAME;
183 | `
184 | 
185 | func init() {
186 | 	if !tools.Register(kind, newConfig) {
187 | 		panic(fmt.Sprintf("tool kind %q already registered", kind))
188 | 	}
189 | }
190 | 
191 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
192 | 	actual := Config{Name: name}
193 | 	if err := decoder.DecodeContext(ctx, &actual); err != nil {
194 | 		return nil, err
195 | 	}
196 | 	return actual, nil
197 | }
198 | 
199 | type compatibleSource interface {
200 | 	MySQLPool() *sql.DB
201 | }
202 | 
203 | // validate compatible sources are still compatible
204 | var _ compatibleSource = &cloudsqlmysql.Source{}
205 | var _ compatibleSource = &mysql.Source{}
206 | 
207 | var compatibleSources = [...]string{cloudsqlmysql.SourceKind, mysql.SourceKind}
208 | 
209 | type Config struct {
210 | 	Name         string   `yaml:"name" validate:"required"`
211 | 	Kind         string   `yaml:"kind" validate:"required"`
212 | 	Source       string   `yaml:"source" validate:"required"`
213 | 	Description  string   `yaml:"description" validate:"required"`
214 | 	AuthRequired []string `yaml:"authRequired"`
215 | }
216 | 
217 | // validate interface
218 | var _ tools.ToolConfig = Config{}
219 | 
220 | func (cfg Config) ToolConfigKind() string {
221 | 	return kind
222 | }
223 | 
224 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
225 | 	// verify source exists
226 | 	rawS, ok := srcs[cfg.Source]
227 | 	if !ok {
228 | 		return nil, fmt.Errorf("no source named %q configured", cfg.Source)
229 | 	}
230 | 
231 | 	// verify the source is compatible
232 | 	s, ok := rawS.(compatibleSource)
233 | 	if !ok {
234 | 		return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
235 | 	}
236 | 
237 | 	allParameters := tools.Parameters{
238 | 		tools.NewStringParameterWithDefault("table_names", "", "Optional: A comma-separated list of table names. If empty, details for all tables will be listed."),
239 | 		tools.NewStringParameterWithDefault("output_format", "detailed", "Optional: Use 'simple' for names only or 'detailed' for full info."),
240 | 	}
241 | 	paramManifest := allParameters.Manifest()
242 | 	mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
243 | 
244 | 	// finish tool setup
245 | 	t := Tool{
246 | 		Name:         cfg.Name,
247 | 		Kind:         kind,
248 | 		AllParams:    allParameters,
249 | 		AuthRequired: cfg.AuthRequired,
250 | 		Pool:         s.MySQLPool(),
251 | 		manifest:     tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
252 | 		mcpManifest:  mcpManifest,
253 | 	}
254 | 	return t, nil
255 | }
256 | 
257 | // validate interface
258 | var _ tools.Tool = Tool{}
259 | 
260 | type Tool struct {
261 | 	Name         string           `yaml:"name"`
262 | 	Kind         string           `yaml:"kind"`
263 | 	AuthRequired []string         `yaml:"authRequired"`
264 | 	AllParams    tools.Parameters `yaml:"allParams"`
265 | 
266 | 	Pool        *sql.DB
267 | 	manifest    tools.Manifest
268 | 	mcpManifest tools.McpManifest
269 | }
270 | 
271 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
272 | 	paramsMap := params.AsMap()
273 | 
274 | 	tableNames, ok := paramsMap["table_names"].(string)
275 | 	if !ok {
276 | 		return nil, fmt.Errorf("invalid '%s' parameter; expected a string", tableNames)
277 | 	}
278 | 	outputFormat, _ := paramsMap["output_format"].(string)
279 | 	if outputFormat != "simple" && outputFormat != "detailed" {
280 | 		return nil, fmt.Errorf("invalid value for output_format: must be 'simple' or 'detailed', but got %q", outputFormat)
281 | 	}
282 | 
283 | 	results, err := t.Pool.QueryContext(ctx, listTablesStatement, tableNames, outputFormat)
284 | 	if err != nil {
285 | 		return nil, fmt.Errorf("unable to execute query: %w", err)
286 | 	}
287 | 
288 | 	cols, err := results.Columns()
289 | 	if err != nil {
290 | 		return nil, fmt.Errorf("unable to retrieve rows column name: %w", err)
291 | 	}
292 | 
293 | 	// create an array of values for each column, which can be re-used to scan each row
294 | 	rawValues := make([]any, len(cols))
295 | 	values := make([]any, len(cols))
296 | 	for i := range rawValues {
297 | 		values[i] = &rawValues[i]
298 | 	}
299 | 	defer results.Close()
300 | 
301 | 	colTypes, err := results.ColumnTypes()
302 | 	if err != nil {
303 | 		return nil, fmt.Errorf("unable to get column types: %w", err)
304 | 	}
305 | 
306 | 	var out []any
307 | 	for results.Next() {
308 | 		err := results.Scan(values...)
309 | 		if err != nil {
310 | 			return nil, fmt.Errorf("unable to parse row: %w", err)
311 | 		}
312 | 		vMap := make(map[string]any)
313 | 		for i, name := range cols {
314 | 			val := rawValues[i]
315 | 			if val == nil {
316 | 				vMap[name] = nil
317 | 				continue
318 | 			}
319 | 
320 | 			vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val)
321 | 			if err != nil {
322 | 				return nil, fmt.Errorf("errors encountered when converting values: %w", err)
323 | 			}
324 | 		}
325 | 		out = append(out, vMap)
326 | 	}
327 | 
328 | 	if err := results.Err(); err != nil {
329 | 		return nil, fmt.Errorf("errors encountered during row iteration: %w", err)
330 | 	}
331 | 
332 | 	return out, nil
333 | }
334 | 
335 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
336 | 	return tools.ParseParams(t.AllParams, data, claims)
337 | }
338 | 
339 | func (t Tool) Manifest() tools.Manifest {
340 | 	return t.manifest
341 | }
342 | 
343 | func (t Tool) McpManifest() tools.McpManifest {
344 | 	return t.mcpManifest
345 | }
346 | 
347 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
348 | 	return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
349 | }
350 | 
351 | func (t Tool) RequiresClientAuthorization() bool {
352 | 	return false
353 | }
354 | 
```

--------------------------------------------------------------------------------
/internal/tools/looker/lookerhealthpulse/lookerhealthpulse.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 lookerhealthpulse
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"encoding/json"
 19 | 	"fmt"
 20 | 	"strings"
 21 | 
 22 | 	yaml "github.com/goccy/go-yaml"
 23 | 	"github.com/googleapis/genai-toolbox/internal/sources"
 24 | 	lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker"
 25 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 26 | 	"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
 27 | 	"github.com/googleapis/genai-toolbox/internal/util"
 28 | 
 29 | 	"github.com/looker-open-source/sdk-codegen/go/rtl"
 30 | 	v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
 31 | )
 32 | 
 33 | // =================================================================================================================
 34 | // START MCP SERVER CORE LOGIC
 35 | // =================================================================================================================
 36 | const kind string = "looker-health-pulse"
 37 | 
 38 | func init() {
 39 | 	if !tools.Register(kind, newConfig) {
 40 | 		panic(fmt.Sprintf("tool kind %q already registered", kind))
 41 | 	}
 42 | }
 43 | 
 44 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
 45 | 	actual := Config{Name: name}
 46 | 	if err := decoder.DecodeContext(ctx, &actual); err != nil {
 47 | 		return nil, err
 48 | 	}
 49 | 	return actual, nil
 50 | }
 51 | 
 52 | type Config struct {
 53 | 	Name         string         `yaml:"name" validate:"required"`
 54 | 	Kind         string         `yaml:"kind" validate:"required"`
 55 | 	Source       string         `yaml:"source" validate:"required"`
 56 | 	Description  string         `yaml:"description" validate:"required"`
 57 | 	AuthRequired []string       `yaml:"authRequired"`
 58 | 	Parameters   map[string]any `yaml:"parameters"`
 59 | }
 60 | 
 61 | // validate interface
 62 | var _ tools.ToolConfig = Config{}
 63 | 
 64 | func (cfg Config) ToolConfigKind() string {
 65 | 	return kind
 66 | }
 67 | 
 68 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
 69 | 	// verify source exists
 70 | 	rawS, ok := srcs[cfg.Source]
 71 | 	if !ok {
 72 | 		return nil, fmt.Errorf("no source named %q configured", cfg.Source)
 73 | 	}
 74 | 
 75 | 	// verify the source is compatible
 76 | 	s, ok := rawS.(*lookersrc.Source)
 77 | 	if !ok {
 78 | 		return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind)
 79 | 	}
 80 | 
 81 | 	actionParameter := tools.NewStringParameterWithRequired("action", "The health check to run. Can be either: `check_db_connections`, `check_dashboard_performance`,`check_dashboard_errors`,`check_explore_performance`,`check_schedule_failures`, or `check_legacy_features`", true)
 82 | 
 83 | 	parameters := tools.Parameters{
 84 | 		actionParameter,
 85 | 	}
 86 | 
 87 | 	mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
 88 | 
 89 | 	// finish tool setup
 90 | 	return Tool{
 91 | 		Name:           cfg.Name,
 92 | 		Kind:           kind,
 93 | 		Parameters:     parameters,
 94 | 		AuthRequired:   cfg.AuthRequired,
 95 | 		UseClientOAuth: s.UseClientOAuth,
 96 | 		Client:         s.Client,
 97 | 		ApiSettings:    s.ApiSettings,
 98 | 		manifest: tools.Manifest{
 99 | 			Description:  cfg.Description,
100 | 			Parameters:   parameters.Manifest(),
101 | 			AuthRequired: cfg.AuthRequired,
102 | 		},
103 | 		mcpManifest: mcpManifest,
104 | 	}, nil
105 | }
106 | 
107 | // validate interface
108 | var _ tools.Tool = Tool{}
109 | 
110 | type Tool struct {
111 | 	Name           string `yaml:"name"`
112 | 	Kind           string `yaml:"kind"`
113 | 	UseClientOAuth bool
114 | 	Client         *v4.LookerSDK
115 | 	ApiSettings    *rtl.ApiSettings
116 | 	AuthRequired   []string         `yaml:"authRequired"`
117 | 	Parameters     tools.Parameters `yaml:"parameters"`
118 | 	manifest       tools.Manifest
119 | 	mcpManifest    tools.McpManifest
120 | }
121 | 
122 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
123 | 	logger, err := util.LoggerFromContext(ctx)
124 | 	if err != nil {
125 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
126 | 	}
127 | 
128 | 	sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
129 | 	if err != nil {
130 | 		return nil, fmt.Errorf("error getting sdk: %w", err)
131 | 	}
132 | 
133 | 	pulseTool := &pulseTool{
134 | 		ApiSettings: t.ApiSettings,
135 | 		SdkClient:   sdk,
136 | 	}
137 | 
138 | 	paramsMap := params.AsMap()
139 | 	action, ok := paramsMap["action"].(string)
140 | 	if !ok {
141 | 		return nil, fmt.Errorf("action parameter not found")
142 | 	}
143 | 
144 | 	pulseParams := PulseParams{
145 | 		Action: action,
146 | 	}
147 | 
148 | 	result, err := pulseTool.RunPulse(ctx, pulseParams)
149 | 	if err != nil {
150 | 		return nil, fmt.Errorf("error running pulse: %w", err)
151 | 	}
152 | 
153 | 	logger.DebugContext(ctx, "result = ", result)
154 | 
155 | 	return result, nil
156 | }
157 | 
158 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
159 | 	return tools.ParseParams(t.Parameters, data, claims)
160 | }
161 | 
162 | func (t Tool) Manifest() tools.Manifest {
163 | 	return t.manifest
164 | }
165 | 
166 | func (t Tool) McpManifest() tools.McpManifest {
167 | 	return t.mcpManifest
168 | }
169 | 
170 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
171 | 	return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
172 | }
173 | 
174 | func (t Tool) RequiresClientAuthorization() bool {
175 | 	return t.UseClientOAuth
176 | }
177 | 
178 | // =================================================================================================================
179 | // END MCP SERVER CORE LOGIC
180 | // =================================================================================================================
181 | 
182 | // =================================================================================================================
183 | // START LOOKER HEALTH PULSE CORE LOGIC
184 | // =================================================================================================================
185 | type PulseParams struct {
186 | 	Action string
187 | 	// Optionally add more parameters if needed
188 | }
189 | 
190 | // pulseTool holds Looker API settings and client
191 | type pulseTool struct {
192 | 	ApiSettings *rtl.ApiSettings
193 | 	SdkClient   *v4.LookerSDK
194 | }
195 | 
196 | func (t *pulseTool) RunPulse(ctx context.Context, params PulseParams) (interface{}, error) {
197 | 	switch params.Action {
198 | 	case "check_db_connections":
199 | 		return t.checkDBConnections(ctx)
200 | 	case "check_dashboard_performance":
201 | 		return t.checkDashboardPerformance(ctx)
202 | 	case "check_dashboard_errors":
203 | 		return t.checkDashboardErrors(ctx)
204 | 	case "check_explore_performance":
205 | 		return t.checkExplorePerformance(ctx)
206 | 	case "check_schedule_failures":
207 | 		return t.checkScheduleFailures(ctx)
208 | 	case "check_legacy_features":
209 | 		return t.checkLegacyFeatures(ctx)
210 | 	default:
211 | 		return nil, fmt.Errorf("unknown action: %s", params.Action)
212 | 	}
213 | }
214 | 
215 | // Check DB connections and run tests
216 | func (t *pulseTool) checkDBConnections(ctx context.Context) (interface{}, error) {
217 | 	logger, err := util.LoggerFromContext(ctx)
218 | 	if err != nil {
219 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
220 | 	}
221 | 	logger.InfoContext(ctx, "Test 1/6: Checking connections")
222 | 
223 | 	reservedNames := map[string]struct{}{
224 | 		"looker__internal__analytics__replica": {},
225 | 		"looker__internal__analytics":          {},
226 | 		"looker":                               {},
227 | 		"looker__ilooker":                      {},
228 | 	}
229 | 
230 | 	connections, err := t.SdkClient.AllConnections("", t.ApiSettings)
231 | 	if err != nil {
232 | 		return nil, fmt.Errorf("error fetching connections: %w", err)
233 | 	}
234 | 
235 | 	var filteredConnections []v4.DBConnection
236 | 	for _, c := range connections {
237 | 		if _, reserved := reservedNames[*c.Name]; !reserved {
238 | 			filteredConnections = append(filteredConnections, c)
239 | 		}
240 | 	}
241 | 	if len(filteredConnections) == 0 {
242 | 		return nil, fmt.Errorf("no connections found")
243 | 	}
244 | 
245 | 	var results []map[string]interface{}
246 | 	for _, conn := range filteredConnections {
247 | 		var errors []string
248 | 		// Test connection (simulate test_connection endpoint)
249 | 		resp, err := t.SdkClient.TestConnection(*conn.Name, nil, t.ApiSettings)
250 | 		if err != nil {
251 | 			errors = append(errors, "API JSONDecode Error")
252 | 		} else {
253 | 			for _, r := range resp {
254 | 				if *r.Status == "error" {
255 | 					errors = append(errors, *r.Message)
256 | 				}
257 | 			}
258 | 		}
259 | 
260 | 		// Run inline query for connection activity
261 | 		limit := "1"
262 | 		query := &v4.WriteQuery{
263 | 			Model:  "system__activity",
264 | 			View:   "history",
265 | 			Fields: &[]string{"history.query_run_count"},
266 | 			Filters: &map[string]any{
267 | 				"history.connection_name": *conn.Name,
268 | 				"history.created_date":    "90 days",
269 | 				"user.dev_branch_name":    "NULL",
270 | 			},
271 | 			Limit: &limit,
272 | 		}
273 | 		raw, err := lookercommon.RunInlineQuery(ctx, t.SdkClient, query, "json", t.ApiSettings)
274 | 		if err != nil {
275 | 			return nil, err
276 | 		}
277 | 		var queryRunCount interface{}
278 | 		var data []map[string]interface{}
279 | 		_ = json.Unmarshal([]byte(raw), &data)
280 | 		if len(data) > 0 {
281 | 			queryRunCount = data[0]["history.query_run_count"]
282 | 		}
283 | 
284 | 		results = append(results, map[string]interface{}{
285 | 			"Connection":  *conn.Name,
286 | 			"Status":      "OK",
287 | 			"Errors":      errors,
288 | 			"Query Count": queryRunCount,
289 | 		})
290 | 	}
291 | 	return results, nil
292 | }
293 | 
294 | func (t *pulseTool) checkDashboardPerformance(ctx context.Context) (interface{}, error) {
295 | 	logger, err := util.LoggerFromContext(ctx)
296 | 	if err != nil {
297 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
298 | 	}
299 | 	logger.InfoContext(ctx, "Test 2/6: Checking for dashboards with queries slower than 30 seconds in the last 7 days")
300 | 
301 | 	limit := "20"
302 | 	query := &v4.WriteQuery{
303 | 		Model:  "system__activity",
304 | 		View:   "history",
305 | 		Fields: &[]string{"dashboard.title", "query.count"},
306 | 		Filters: &map[string]any{
307 | 			"history.created_date": "7 days",
308 | 			"history.real_dash_id": "-NULL",
309 | 			"history.runtime":      ">30",
310 | 			"history.status":       "complete",
311 | 		},
312 | 		Sorts: &[]string{"query.count desc"},
313 | 		Limit: &limit,
314 | 	}
315 | 	raw, err := lookercommon.RunInlineQuery(ctx, t.SdkClient, query, "json", t.ApiSettings)
316 | 	if err != nil {
317 | 		return nil, err
318 | 	}
319 | 	var dashboards []map[string]interface{}
320 | 	if err := json.Unmarshal([]byte(raw), &dashboards); err != nil {
321 | 		return nil, err
322 | 	}
323 | 	return dashboards, nil
324 | }
325 | 
326 | func (t *pulseTool) checkDashboardErrors(ctx context.Context) (interface{}, error) {
327 | 	logger, err := util.LoggerFromContext(ctx)
328 | 	if err != nil {
329 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
330 | 	}
331 | 	logger.InfoContext(ctx, "Test 3/6: Checking for dashboards with erroring queries in the last 7 days")
332 | 
333 | 	limit := "20"
334 | 	query := &v4.WriteQuery{
335 | 		Model:  "system__activity",
336 | 		View:   "history",
337 | 		Fields: &[]string{"dashboard.title", "history.query_run_count"},
338 | 		Filters: &map[string]any{
339 | 			"dashboard.title":           "-NULL",
340 | 			"history.created_date":      "7 days",
341 | 			"history.dashboard_session": "-NULL",
342 | 			"history.status":            "error",
343 | 		},
344 | 		Sorts: &[]string{"history.query_run_count desc"},
345 | 		Limit: &limit,
346 | 	}
347 | 	raw, err := lookercommon.RunInlineQuery(ctx, t.SdkClient, query, "json", t.ApiSettings)
348 | 	if err != nil {
349 | 		return nil, err
350 | 	}
351 | 	var dashboards []map[string]interface{}
352 | 	if err := json.Unmarshal([]byte(raw), &dashboards); err != nil {
353 | 		return nil, err
354 | 	}
355 | 	return dashboards, nil
356 | }
357 | 
358 | func (t *pulseTool) checkExplorePerformance(ctx context.Context) (interface{}, error) {
359 | 	logger, err := util.LoggerFromContext(ctx)
360 | 	if err != nil {
361 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
362 | 	}
363 | 	logger.InfoContext(ctx, "Test 4/6: Checking for the slowest explores in the past 7 days")
364 | 
365 | 	limit := "20"
366 | 	query := &v4.WriteQuery{
367 | 		Model:  "system__activity",
368 | 		View:   "history",
369 | 		Fields: &[]string{"query.model", "query.view", "history.average_runtime"},
370 | 		Filters: &map[string]any{
371 | 			"history.created_date": "7 days",
372 | 			"query.model":          "-NULL, -system^_^_activity",
373 | 		},
374 | 		Sorts: &[]string{"history.average_runtime desc"},
375 | 		Limit: &limit,
376 | 	}
377 | 	raw, err := lookercommon.RunInlineQuery(ctx, t.SdkClient, query, "json", t.ApiSettings)
378 | 	if err != nil {
379 | 		return nil, err
380 | 	}
381 | 	var explores []map[string]interface{}
382 | 	if err := json.Unmarshal([]byte(raw), &explores); err != nil {
383 | 		return nil, err
384 | 	}
385 | 
386 | 	// Average query runtime
387 | 	query.Fields = &[]string{"history.average_runtime"}
388 | 	rawAvg, err := lookercommon.RunInlineQuery(ctx, t.SdkClient, query, "json", t.ApiSettings)
389 | 	if err != nil {
390 | 		return nil, err
391 | 	}
392 | 	var avgData []map[string]interface{}
393 | 	if err := json.Unmarshal([]byte(rawAvg), &avgData); err == nil {
394 | 		if len(avgData) > 0 {
395 | 			if avgRuntime, ok := avgData[0]["history.average_runtime"].(float64); ok {
396 | 				logger.InfoContext(ctx, fmt.Sprintf("For context, the average query runtime is %.4fs", avgRuntime))
397 | 			}
398 | 		}
399 | 	}
400 | 	return explores, nil
401 | }
402 | 
403 | func (t *pulseTool) checkScheduleFailures(ctx context.Context) (interface{}, error) {
404 | 	logger, err := util.LoggerFromContext(ctx)
405 | 	if err != nil {
406 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
407 | 	}
408 | 	logger.InfoContext(ctx, "Test 5/6: Checking for failing schedules")
409 | 
410 | 	limit := "500"
411 | 	query := &v4.WriteQuery{
412 | 		Model:  "system__activity",
413 | 		View:   "scheduled_plan",
414 | 		Fields: &[]string{"scheduled_job.name", "scheduled_job.count"},
415 | 		Filters: &map[string]any{
416 | 			"scheduled_job.created_date": "7 days",
417 | 			"scheduled_job.status":       "failure",
418 | 		},
419 | 		Sorts: &[]string{"scheduled_job.count desc"},
420 | 		Limit: &limit,
421 | 	}
422 | 	raw, err := lookercommon.RunInlineQuery(ctx, t.SdkClient, query, "json", t.ApiSettings)
423 | 	if err != nil {
424 | 		return nil, err
425 | 	}
426 | 	var schedules []map[string]interface{}
427 | 	if err := json.Unmarshal([]byte(raw), &schedules); err != nil {
428 | 		return nil, err
429 | 	}
430 | 	return schedules, nil
431 | }
432 | 
433 | func (t *pulseTool) checkLegacyFeatures(ctx context.Context) (interface{}, error) {
434 | 	logger, err := util.LoggerFromContext(ctx)
435 | 	if err != nil {
436 | 		return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
437 | 	}
438 | 	logger.InfoContext(ctx, "Test 6/6: Checking for enabled legacy features")
439 | 
440 | 	features, err := t.SdkClient.AllLegacyFeatures(t.ApiSettings)
441 | 	if err != nil {
442 | 		if strings.Contains(err.Error(), "Unsupported in Looker (Google Cloud core)") {
443 | 			return []map[string]string{{"Feature": "Unsupported in Looker (Google Cloud core)"}}, nil
444 | 		}
445 | 		logger.ErrorContext(ctx, err.Error())
446 | 		return []map[string]string{{"Feature": "Unable to pull legacy features due to SDK error"}}, nil
447 | 	}
448 | 	var legacyFeatures []map[string]string
449 | 	for _, f := range features {
450 | 		if *f.Enabled {
451 | 			legacyFeatures = append(legacyFeatures, map[string]string{"Feature": *f.Name})
452 | 		}
453 | 	}
454 | 	return legacyFeatures, nil
455 | }
456 | 
457 | // =================================================================================================================
458 | // END LOOKER HEALTH PULSE CORE LOGIC
459 | // =================================================================================================================
460 | 
```

--------------------------------------------------------------------------------
/internal/server/mcp.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 server
 16 | 
 17 | import (
 18 | 	"bufio"
 19 | 	"bytes"
 20 | 	"context"
 21 | 	"encoding/json"
 22 | 	"errors"
 23 | 	"fmt"
 24 | 	"io"
 25 | 	"net/http"
 26 | 	"strings"
 27 | 	"sync"
 28 | 	"time"
 29 | 
 30 | 	"github.com/go-chi/chi/v5"
 31 | 	"github.com/go-chi/chi/v5/middleware"
 32 | 	"github.com/go-chi/render"
 33 | 	"github.com/google/uuid"
 34 | 	"github.com/googleapis/genai-toolbox/internal/server/mcp"
 35 | 	"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
 36 | 	mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
 37 | 	v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
 38 | 	v20250326 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250326"
 39 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 40 | 	"github.com/googleapis/genai-toolbox/internal/util"
 41 | 	"go.opentelemetry.io/otel/attribute"
 42 | 	"go.opentelemetry.io/otel/codes"
 43 | 	"go.opentelemetry.io/otel/metric"
 44 | )
 45 | 
 46 | type sseSession struct {
 47 | 	writer     http.ResponseWriter
 48 | 	flusher    http.Flusher
 49 | 	done       chan struct{}
 50 | 	eventQueue chan string
 51 | 	lastActive time.Time
 52 | }
 53 | 
 54 | // sseManager manages and control access to sse sessions
 55 | type sseManager struct {
 56 | 	mu          sync.Mutex
 57 | 	sseSessions map[string]*sseSession
 58 | }
 59 | 
 60 | func (m *sseManager) get(id string) (*sseSession, bool) {
 61 | 	m.mu.Lock()
 62 | 	defer m.mu.Unlock()
 63 | 	session, ok := m.sseSessions[id]
 64 | 	session.lastActive = time.Now()
 65 | 	return session, ok
 66 | }
 67 | 
 68 | func newSseManager(ctx context.Context) *sseManager {
 69 | 	sseM := &sseManager{
 70 | 		mu:          sync.Mutex{},
 71 | 		sseSessions: make(map[string]*sseSession),
 72 | 	}
 73 | 	go sseM.cleanupRoutine(ctx)
 74 | 	return sseM
 75 | }
 76 | 
 77 | func (m *sseManager) add(id string, session *sseSession) {
 78 | 	m.mu.Lock()
 79 | 	defer m.mu.Unlock()
 80 | 	m.sseSessions[id] = session
 81 | 	session.lastActive = time.Now()
 82 | }
 83 | 
 84 | func (m *sseManager) remove(id string) {
 85 | 	m.mu.Lock()
 86 | 	delete(m.sseSessions, id)
 87 | 	m.mu.Unlock()
 88 | }
 89 | 
 90 | func (m *sseManager) cleanupRoutine(ctx context.Context) {
 91 | 	timeout := 10 * time.Minute
 92 | 	ticker := time.NewTicker(timeout)
 93 | 	defer ticker.Stop()
 94 | 
 95 | 	for {
 96 | 		select {
 97 | 		case <-ctx.Done():
 98 | 			return
 99 | 		case <-ticker.C:
100 | 			func() {
101 | 				m.mu.Lock()
102 | 				defer m.mu.Unlock()
103 | 				now := time.Now()
104 | 				for id, sess := range m.sseSessions {
105 | 					if now.Sub(sess.lastActive) > timeout {
106 | 						delete(m.sseSessions, id)
107 | 					}
108 | 				}
109 | 			}()
110 | 		}
111 | 	}
112 | }
113 | 
114 | type stdioSession struct {
115 | 	protocol string
116 | 	server   *Server
117 | 	reader   *bufio.Reader
118 | 	writer   io.Writer
119 | }
120 | 
121 | func NewStdioSession(s *Server, stdin io.Reader, stdout io.Writer) *stdioSession {
122 | 	stdioSession := &stdioSession{
123 | 		server: s,
124 | 		reader: bufio.NewReader(stdin),
125 | 		writer: stdout,
126 | 	}
127 | 	return stdioSession
128 | }
129 | 
130 | func (s *stdioSession) Start(ctx context.Context) error {
131 | 	return s.readInputStream(ctx)
132 | }
133 | 
134 | // readInputStream reads requests/notifications from MCP clients through stdin
135 | func (s *stdioSession) readInputStream(ctx context.Context) error {
136 | 	for {
137 | 		if err := ctx.Err(); err != nil {
138 | 			return err
139 | 		}
140 | 		line, err := s.readLine(ctx)
141 | 		if err != nil {
142 | 			if err == io.EOF {
143 | 				return nil
144 | 			}
145 | 			return err
146 | 		}
147 | 		v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", nil)
148 | 		if err != nil {
149 | 			// errors during the processing of message will generate a valid MCP Error response.
150 | 			// server can continue to run.
151 | 			s.server.logger.ErrorContext(ctx, err.Error())
152 | 		}
153 | 		if v != "" {
154 | 			s.protocol = v
155 | 		}
156 | 		// no responses for notifications
157 | 		if res != nil {
158 | 			if err = s.write(ctx, res); err != nil {
159 | 				return err
160 | 			}
161 | 		}
162 | 	}
163 | }
164 | 
165 | // readLine process each line within the input stream.
166 | func (s *stdioSession) readLine(ctx context.Context) (string, error) {
167 | 	readChan := make(chan string, 1)
168 | 	errChan := make(chan error, 1)
169 | 	done := make(chan struct{})
170 | 	defer close(done)
171 | 	defer close(readChan)
172 | 	defer close(errChan)
173 | 
174 | 	go func() {
175 | 		select {
176 | 		case <-done:
177 | 			return
178 | 		default:
179 | 			line, err := s.reader.ReadString('\n')
180 | 			if err != nil {
181 | 				select {
182 | 				case errChan <- err:
183 | 				case <-done:
184 | 				}
185 | 				return
186 | 			}
187 | 			select {
188 | 			case readChan <- line:
189 | 			case <-done:
190 | 			}
191 | 			return
192 | 		}
193 | 	}()
194 | 
195 | 	select {
196 | 	// if context is cancelled, return an empty string
197 | 	case <-ctx.Done():
198 | 		return "", ctx.Err()
199 | 	// return error if error is found
200 | 	case err := <-errChan:
201 | 		return "", err
202 | 	// return line if successful
203 | 	case line := <-readChan:
204 | 		return line, nil
205 | 	}
206 | }
207 | 
208 | // write writes to stdout with response to client
209 | func (s *stdioSession) write(ctx context.Context, response any) error {
210 | 	res, _ := json.Marshal(response)
211 | 
212 | 	_, err := fmt.Fprintf(s.writer, "%s\n", res)
213 | 	return err
214 | }
215 | 
216 | // mcpRouter creates a router that represents the routes under /mcp
217 | func mcpRouter(s *Server) (chi.Router, error) {
218 | 	r := chi.NewRouter()
219 | 
220 | 	r.Use(middleware.AllowContentType("application/json", "application/json-rpc", "application/jsonrequest"))
221 | 	r.Use(middleware.StripSlashes)
222 | 	r.Use(render.SetContentType(render.ContentTypeJSON))
223 | 
224 | 	r.Get("/sse", func(w http.ResponseWriter, r *http.Request) { sseHandler(s, w, r) })
225 | 	r.Get("/", func(w http.ResponseWriter, r *http.Request) { methodNotAllowed(s, w, r) })
226 | 	r.Post("/", func(w http.ResponseWriter, r *http.Request) { httpHandler(s, w, r) })
227 | 	r.Delete("/", func(w http.ResponseWriter, r *http.Request) {})
228 | 
229 | 	r.Route("/{toolsetName}", func(r chi.Router) {
230 | 		r.Get("/sse", func(w http.ResponseWriter, r *http.Request) { sseHandler(s, w, r) })
231 | 		r.Get("/", func(w http.ResponseWriter, r *http.Request) { methodNotAllowed(s, w, r) })
232 | 		r.Post("/", func(w http.ResponseWriter, r *http.Request) { httpHandler(s, w, r) })
233 | 		r.Delete("/", func(w http.ResponseWriter, r *http.Request) {})
234 | 	})
235 | 
236 | 	return r, nil
237 | }
238 | 
239 | // sseHandler handles sse initialization and message.
240 | func sseHandler(s *Server, w http.ResponseWriter, r *http.Request) {
241 | 	ctx, span := s.instrumentation.Tracer.Start(r.Context(), "toolbox/server/mcp/sse")
242 | 	r = r.WithContext(ctx)
243 | 
244 | 	sessionId := uuid.New().String()
245 | 	toolsetName := chi.URLParam(r, "toolsetName")
246 | 	s.logger.DebugContext(ctx, fmt.Sprintf("toolset name: %s", toolsetName))
247 | 	span.SetAttributes(attribute.String("session_id", sessionId))
248 | 	span.SetAttributes(attribute.String("toolset_name", toolsetName))
249 | 
250 | 	w.Header().Set("Content-Type", "text/event-stream")
251 | 	w.Header().Set("Cache-Control", "no-cache")
252 | 	w.Header().Set("Connection", "keep-alive")
253 | 	w.Header().Set("Access-Control-Allow-Origin", "*")
254 | 
255 | 	var err error
256 | 	defer func() {
257 | 		if err != nil {
258 | 			span.SetStatus(codes.Error, err.Error())
259 | 		}
260 | 		span.End()
261 | 		status := "success"
262 | 		if err != nil {
263 | 			status = "error"
264 | 		}
265 | 		s.instrumentation.McpSse.Add(
266 | 			r.Context(),
267 | 			1,
268 | 			metric.WithAttributes(attribute.String("toolbox.toolset.name", toolsetName)),
269 | 			metric.WithAttributes(attribute.String("toolbox.sse.sessionId", sessionId)),
270 | 			metric.WithAttributes(attribute.String("toolbox.operation.status", status)),
271 | 		)
272 | 	}()
273 | 
274 | 	flusher, ok := w.(http.Flusher)
275 | 	if !ok {
276 | 		err = fmt.Errorf("unable to retrieve flusher for sse")
277 | 		s.logger.DebugContext(ctx, err.Error())
278 | 		_ = render.Render(w, r, newErrResponse(err, http.StatusInternalServerError))
279 | 	}
280 | 	session := &sseSession{
281 | 		writer:     w,
282 | 		flusher:    flusher,
283 | 		done:       make(chan struct{}),
284 | 		eventQueue: make(chan string, 100),
285 | 	}
286 | 	s.sseManager.add(sessionId, session)
287 | 	defer s.sseManager.remove(sessionId)
288 | 
289 | 	// https scheme formatting if (forwarded) request is a TLS request
290 | 	proto := r.Header.Get("X-Forwarded-Proto")
291 | 	if proto == "" {
292 | 		if r.TLS == nil {
293 | 			proto = "http"
294 | 		} else {
295 | 			proto = "https"
296 | 		}
297 | 	}
298 | 
299 | 	// send initial endpoint event
300 | 	toolsetURL := ""
301 | 	if toolsetName != "" {
302 | 		toolsetURL = fmt.Sprintf("/%s", toolsetName)
303 | 	}
304 | 	messageEndpoint := fmt.Sprintf("%s://%s/mcp%s?sessionId=%s", proto, r.Host, toolsetURL, sessionId)
305 | 	s.logger.DebugContext(ctx, fmt.Sprintf("sending endpoint event: %s", messageEndpoint))
306 | 	fmt.Fprintf(w, "event: endpoint\ndata: %s\n\n", messageEndpoint)
307 | 	flusher.Flush()
308 | 
309 | 	clientClose := r.Context().Done()
310 | 	for {
311 | 		select {
312 | 		// Ensure that only a single responses are written at once
313 | 		case event := <-session.eventQueue:
314 | 			fmt.Fprint(w, event)
315 | 			s.logger.DebugContext(ctx, fmt.Sprintf("sending event: %s", event))
316 | 			flusher.Flush()
317 | 			// channel for client disconnection
318 | 		case <-clientClose:
319 | 			close(session.done)
320 | 			s.logger.DebugContext(ctx, "client disconnected")
321 | 			return
322 | 		}
323 | 	}
324 | }
325 | 
326 | // methodNotAllowed handles all mcp messages.
327 | func methodNotAllowed(s *Server, w http.ResponseWriter, r *http.Request) {
328 | 	err := fmt.Errorf("toolbox does not support streaming in streamable HTTP transport")
329 | 	s.logger.DebugContext(r.Context(), err.Error())
330 | 	_ = render.Render(w, r, newErrResponse(err, http.StatusMethodNotAllowed))
331 | }
332 | 
333 | // httpHandler handles all mcp messages.
334 | func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
335 | 	w.Header().Set("Content-Type", "application/json")
336 | 
337 | 	ctx, span := s.instrumentation.Tracer.Start(r.Context(), "toolbox/server/mcp")
338 | 	r = r.WithContext(ctx)
339 | 	ctx = util.WithLogger(r.Context(), s.logger)
340 | 
341 | 	var sessionId, protocolVersion string
342 | 	var session *sseSession
343 | 
344 | 	// check if client connects via sse
345 | 	// v2024-11-05 supports http with sse
346 | 	paramSessionId := r.URL.Query().Get("sessionId")
347 | 	if paramSessionId != "" {
348 | 		sessionId = paramSessionId
349 | 		protocolVersion = v20241105.PROTOCOL_VERSION
350 | 		var ok bool
351 | 		session, ok = s.sseManager.get(sessionId)
352 | 		if !ok {
353 | 			s.logger.DebugContext(ctx, "sse session not available")
354 | 		}
355 | 	}
356 | 
357 | 	// check if client have `Mcp-Session-Id` header
358 | 	// `Mcp-Session-Id` is only set for v2025-03-26 in Toolbox
359 | 	headerSessionId := r.Header.Get("Mcp-Session-Id")
360 | 	if headerSessionId != "" {
361 | 		protocolVersion = v20250326.PROTOCOL_VERSION
362 | 	}
363 | 
364 | 	// check if client have `MCP-Protocol-Version` header
365 | 	// Only supported for v2025-06-18+.
366 | 	headerProtocolVersion := r.Header.Get("MCP-Protocol-Version")
367 | 	if headerProtocolVersion != "" {
368 | 		if !mcp.VerifyProtocolVersion(headerProtocolVersion) {
369 | 			err := fmt.Errorf("invalid protocol version: %s", headerProtocolVersion)
370 | 			_ = render.Render(w, r, newErrResponse(err, http.StatusBadRequest))
371 | 			return
372 | 		}
373 | 		protocolVersion = headerProtocolVersion
374 | 	}
375 | 
376 | 	toolsetName := chi.URLParam(r, "toolsetName")
377 | 	s.logger.DebugContext(ctx, fmt.Sprintf("toolset name: %s", toolsetName))
378 | 	span.SetAttributes(attribute.String("toolset_name", toolsetName))
379 | 
380 | 	var err error
381 | 	defer func() {
382 | 		if err != nil {
383 | 			span.SetStatus(codes.Error, err.Error())
384 | 		}
385 | 		span.End()
386 | 
387 | 		status := "success"
388 | 		if err != nil {
389 | 			status = "error"
390 | 		}
391 | 		s.instrumentation.McpPost.Add(
392 | 			r.Context(),
393 | 			1,
394 | 			metric.WithAttributes(attribute.String("toolbox.sse.sessionId", sessionId)),
395 | 			metric.WithAttributes(attribute.String("toolbox.operation.status", status)),
396 | 		)
397 | 	}()
398 | 
399 | 	// Read and returns a body from io.Reader
400 | 	body, err := io.ReadAll(r.Body)
401 | 	if err != nil {
402 | 		// Generate a new uuid if unable to decode
403 | 		id := uuid.New().String()
404 | 		s.logger.DebugContext(ctx, err.Error())
405 | 		render.JSON(w, r, jsonrpc.NewError(id, jsonrpc.PARSE_ERROR, err.Error(), nil))
406 | 		return
407 | 	}
408 | 
409 | 	v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, r.Header)
410 | 	if err != nil {
411 | 		s.logger.DebugContext(ctx, fmt.Errorf("error processing message: %w", err).Error())
412 | 	}
413 | 
414 | 	// notifications will return empty string
415 | 	if res == nil {
416 | 		// Notifications do not expect a response
417 | 		// Toolbox doesn't do anything with notifications yet
418 | 		w.WriteHeader(http.StatusAccepted)
419 | 		return
420 | 	}
421 | 
422 | 	// for v20250326, add the `Mcp-Session-Id` header
423 | 	if v == v20250326.PROTOCOL_VERSION {
424 | 		sessionId = uuid.New().String()
425 | 		w.Header().Set("Mcp-Session-Id", sessionId)
426 | 	}
427 | 
428 | 	if session != nil {
429 | 		// queue sse event
430 | 		eventData, _ := json.Marshal(res)
431 | 		select {
432 | 		case session.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData):
433 | 			s.logger.DebugContext(ctx, "event queue successful")
434 | 		case <-session.done:
435 | 			s.logger.DebugContext(ctx, "session is close")
436 | 		default:
437 | 			s.logger.DebugContext(ctx, "unable to add to event queue")
438 | 		}
439 | 	}
440 | 	if rpcResponse, ok := res.(jsonrpc.JSONRPCError); ok {
441 | 		code := rpcResponse.Error.Code
442 | 		switch code {
443 | 		case jsonrpc.INTERNAL_ERROR:
444 | 			w.WriteHeader(http.StatusInternalServerError)
445 | 		case jsonrpc.INVALID_REQUEST:
446 | 			errStr := err.Error()
447 | 			if errors.Is(err, tools.ErrUnauthorized) {
448 | 				w.WriteHeader(http.StatusUnauthorized)
449 | 			} else if strings.Contains(errStr, "Error 401") {
450 | 				w.WriteHeader(http.StatusUnauthorized)
451 | 			} else if strings.Contains(errStr, "Error 403") {
452 | 				w.WriteHeader(http.StatusForbidden)
453 | 			}
454 | 		}
455 | 	}
456 | 
457 | 	// send HTTP response
458 | 	render.JSON(w, r, res)
459 | }
460 | 
461 | // processMcpMessage process the messages received from clients
462 | func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, header http.Header) (string, any, error) {
463 | 	logger, err := util.LoggerFromContext(ctx)
464 | 	if err != nil {
465 | 		return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
466 | 	}
467 | 
468 | 	// Generic baseMessage could either be a JSONRPCNotification or JSONRPCRequest
469 | 	var baseMessage jsonrpc.BaseMessage
470 | 	if err = util.DecodeJSON(bytes.NewBuffer(body), &baseMessage); err != nil {
471 | 		// Generate a new uuid if unable to decode
472 | 		id := uuid.New().String()
473 | 
474 | 		// check if user is sending a batch request
475 | 		var a []any
476 | 		unmarshalErr := json.Unmarshal(body, &a)
477 | 		if unmarshalErr == nil {
478 | 			err = fmt.Errorf("not supporting batch requests")
479 | 			return "", jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
480 | 		}
481 | 
482 | 		return "", jsonrpc.NewError(id, jsonrpc.PARSE_ERROR, err.Error(), nil), err
483 | 	}
484 | 
485 | 	// Check if method is present
486 | 	if baseMessage.Method == "" {
487 | 		err = fmt.Errorf("method not found")
488 | 		return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
489 | 	}
490 | 	logger.DebugContext(ctx, fmt.Sprintf("method is: %s", baseMessage.Method))
491 | 
492 | 	// Check for JSON-RPC 2.0
493 | 	if baseMessage.Jsonrpc != jsonrpc.JSONRPC_VERSION {
494 | 		err = fmt.Errorf("invalid json-rpc version")
495 | 		return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
496 | 	}
497 | 
498 | 	// Check if message is a notification
499 | 	if baseMessage.Id == nil {
500 | 		err := mcp.NotificationHandler(ctx, body)
501 | 		return "", nil, err
502 | 	}
503 | 
504 | 	switch baseMessage.Method {
505 | 	case mcputil.INITIALIZE:
506 | 		res, v, err := mcp.InitializeResponse(ctx, baseMessage.Id, body, s.version)
507 | 		if err != nil {
508 | 			return "", res, err
509 | 		}
510 | 		return v, res, err
511 | 	default:
512 | 		toolset, ok := s.ResourceMgr.GetToolset(toolsetName)
513 | 		if !ok {
514 | 			err = fmt.Errorf("toolset does not exist")
515 | 			return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
516 | 		}
517 | 		res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), s.ResourceMgr.GetAuthServiceMap(), body, header)
518 | 		return "", res, err
519 | 	}
520 | }
521 | 
```

--------------------------------------------------------------------------------
/DEVELOPER.md:
--------------------------------------------------------------------------------

```markdown
  1 | # DEVELOPER.md
  2 | 
  3 | This document provides instructions for setting up your development environment
  4 | and contributing to the Toolbox project.
  5 | 
  6 | ## Prerequisites
  7 | 
  8 | Before you begin, ensure you have the following:
  9 | 
 10 | 1. **Databases:** Set up the necessary databases for your development
 11 |    environment.
 12 | 1. **Go:** Install the latest version of [Go](https://go.dev/doc/install).
 13 | 1. **Dependencies:** Download and manage project dependencies:
 14 | 
 15 |     ```bash
 16 |     go get
 17 |     go mod tidy
 18 |     ```
 19 | 
 20 | ## Developing Toolbox
 21 | 
 22 | ### Running from Local Source
 23 | 
 24 | 1. **Configuration:** Create a `tools.yaml` file to configure your sources and
 25 |    tools. See the [Configuration section in the
 26 |    README](./README.md#Configuration) for details.
 27 | 1. **CLI Flags:** List available command-line flags for the Toolbox server:
 28 | 
 29 |     ```bash
 30 |     go run . --help
 31 |     ```
 32 | 
 33 | 1. **Running the Server:** Start the Toolbox server with optional flags. The
 34 |    server listens on port 5000 by default.
 35 | 
 36 |     ```bash
 37 |     go run .
 38 |     ```
 39 | 
 40 | 1. **Testing the Endpoint:** Verify the server is running by sending a request
 41 |    to the endpoint:
 42 | 
 43 |     ```bash
 44 |     curl http://127.0.0.1:5000
 45 |     ```
 46 | 
 47 | ### Tool Naming Conventions
 48 | 
 49 | This section details the purpose and conventions for MCP Toolbox's tools naming
 50 | properties, **tool name** and **tool kind**.
 51 | 
 52 | ```
 53 | cancel_hotel: <- tool name
 54 |     kind: postgres-sql  <- tool kind
 55 |     source: my_pg_source
 56 | ```
 57 | 
 58 | #### Tool Name
 59 | 
 60 | Tool name is the identifier used by a Large Language Model (LLM) to invoke a
 61 | specific tool.
 62 | 
 63 | * Custom tools: The user can define any name they want. The below guidelines
 64 |   do not apply.
 65 | * Pre-built tools: The tool name is predefined and cannot be changed. It
 66 | should follow the guidelines.
 67 | 
 68 | The following guidelines apply to tool names:
 69 | 
 70 | * Should use underscores over hyphens (e.g., `list_collections` instead of
 71 |   `list-collections`).
 72 | * Should not have the product name in the name (e.g., `list_collections` instead
 73 |   of `firestore_list_collections`).
 74 | * Superficial changes are NOT considered as breaking (e.g., changing tool name).
 75 | * Non-superficial changes MAY be considered breaking (e.g. adding new parameters
 76 |   to a function) until they can be validated through extensive testing to ensure
 77 |   they do not negatively impact agent's performances.
 78 | 
 79 | #### Tool Kind
 80 | 
 81 | Tool kind serves as a category or type that a user can assign to a tool.
 82 | 
 83 | The following guidelines apply to tool kinds:
 84 | 
 85 | * Should user hyphens over underscores (e.g. `firestore-list-collections` or
 86 |   `firestore_list_colelctions`).
 87 | * Should use product name in name (e.g. `firestore-list-collections` over
 88 |   `list-collections`).
 89 | * Changes to tool kind are breaking changes and should be avoided.
 90 | 
 91 | ## Testing
 92 | 
 93 | ### Infrastructure
 94 | 
 95 | Toolbox uses both GitHub Actions and Cloud Build to run test workflows. Cloud
 96 | Build is used when Google credentials are required. Cloud Build uses test
 97 | project "toolbox-testing-438616".
 98 | 
 99 | ### Linting
100 | 
101 | Run the lint check to ensure code quality:
102 | 
103 | ```bash
104 | golangci-lint run --fix
105 | ```
106 | 
107 | ### Unit Tests
108 | 
109 | Execute unit tests locally:
110 | 
111 | ```bash
112 | go test -race -v ./...
113 | ```
114 | 
115 | ### Integration Tests
116 | 
117 | #### Running Locally
118 | 
119 | 1. **Environment Variables:** Set the required environment variables. Refer to
120 |    the [Cloud Build testing configuration](./.ci/integration.cloudbuild.yaml)
121 |    for a complete list of variables for each source.
122 |     * `SERVICE_ACCOUNT_EMAIL`: Use your own GCP email.
123 |     * `CLIENT_ID`: Use the Google Cloud SDK application Client ID. Contact
124 |       Toolbox maintainers if you don't have it.
125 | 1. **Running Tests:** Run the integration test for your target source. Specify
126 |    the required Go build tags at the top of each integration test file.
127 | 
128 |     ```shell
129 |     go test -race -v ./tests/<YOUR_TEST_DIR>
130 |     ```
131 | 
132 |     For example, to run the AlloyDB integration test:
133 | 
134 |     ```shell
135 |     go test -race -v ./tests/alloydbpg
136 |     ```
137 | 
138 | 1. **Timeout:** The integration test should have a timeout on the server.
139 |    Look for code like this:
140 | 
141 |    ```go
142 |    ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
143 |    defer cancel()
144 | 
145 |    cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
146 |    if err != nil {
147 |      t.Fatalf("command initialization returned an error: %s", err)
148 |    }
149 |    defer cleanup()
150 |    ```
151 | 
152 |    Be sure to set the timeout to a reasonable value for your tests.
153 | 
154 | #### Running on Pull Requests
155 | 
156 | * **Internal Contributors:** Testing workflows should trigger automatically.
157 | * **External Contributors:** Request Toolbox maintainers to trigger the testing
158 |   workflows on your PR.
159 |   * Maintainers can comment `/gcbrun` to execute the integration tests.
160 |   * Maintainers can add the label `tests:run` to execute the unit tests.
161 | 
162 | #### Test Resources
163 | 
164 | The following databases have been added as test resources. To add a new database
165 | to test against, please contact the Toolbox maintainer team via an issue or PR.
166 | Refer to the [Cloud Build testing
167 | configuration](./.ci/integration.cloudbuild.yaml) for a complete list of
168 | variables for each source.
169 | 
170 | * AlloyDB - setup in the test project
171 |   * AI Natural Language ([setup
172 |     instructions](https://cloud.google.com/alloydb/docs/ai/use-natural-language-generate-sql-queries))
173 |     has been configured for `alloydb-ai-nl` tool tests
174 |   * The Cloud Build service account is a user
175 | * Bigtable - setup in the test project
176 |   * The Cloud Build service account is a user
177 | * BigQuery - setup in the test project
178 |   * The Cloud Build service account is a user
179 | * Cloud SQL Postgres - setup in the test project
180 |   * The Cloud Build service account is a user
181 | * Cloud SQL MySQL - setup in the test project
182 |   * The Cloud Build service account is a user
183 | * Cloud SQL SQL Server - setup in the test project
184 |   * The Cloud Build service account is a user
185 | * Couchbase - setup in the test project via the Marketplace
186 | * DGraph - using the public dgraph interface <https://play.dgraph.io> for
187 |   testing
188 | * Looker
189 |   * The Cloud Build service account is a user for conversational analytics
190 |   * The Looker instance runs under google.com:looker-sandbox.
191 | * Memorystore Redis - setup in the test project using a Memorystore for Redis
192 |   standalone instance
193 |   * Memorystore Redis Cluster, Memorystore Valkey standalone, and Memorystore
194 |     Valkey Cluster instances all require PSC connections, which requires extra
195 |     security setup to connect from Cloud Build. Memorystore Redis standalone is
196 |     the only one allowing PSA connection.
197 |   * The Cloud Build service account is a user
198 | * Memorystore Valkey - setup in the test project using a Memorystore for Redis
199 |   standalone instance
200 |   * The Cloud Build service account is a user
201 | * MySQL - setup in the test project using a Cloud SQL instance
202 | * Neo4j - setup in the test project on a GCE VM
203 | * Postgres - setup in the test project using an AlloyDB instance
204 | * Spanner - setup in the test project
205 |   * The Cloud Build service account is a user
206 | * SQL Server - setup in the test project using a Cloud SQL instance
207 | * SQLite -  setup in the integration test, where we create a temporary database
208 |   file
209 | 
210 | ### Other GitHub Checks
211 | 
212 | * License header check (`.github/header-checker-lint.yml`) - Ensures files have
213 |   the appropriate license
214 | * CLA/google - Ensures the developer has signed the CLA:
215 |   <https://cla.developers.google.com/>
216 | * conventionalcommits.org - Ensures the commit messages are in the correct
217 |   format. This repository uses tool [Release
218 |   Please](https://github.com/googleapis/release-please) to create GitHub
219 |   releases. It does so by parsing your git history, looking for [Conventional
220 |   Commit messages](https://www.conventionalcommits.org/), and creating release
221 |   PRs. Learn more by reading [How should I write my
222 |   commits?](https://github.com/googleapis/release-please?tab=readme-ov-file#how-should-i-write-my-commits)
223 | 
224 | ## Developing Documentation
225 | 
226 | ### Running a Local Hugo Server
227 | 
228 | Follow these steps to preview documentation changes locally using a Hugo server:
229 | 
230 | 1. **Install Hugo:** Ensure you have
231 |    [Hugo](https://gohugo.io/installation/macos/) extended edition version
232 |    0.146.0 or later installed.
233 | 1. **Navigate to the Hugo Directory:**
234 | 
235 |     ```bash
236 |     cd .hugo
237 |     ```
238 | 
239 | 1. **Install Dependencies:**
240 | 
241 |     ```bash
242 |     npm ci
243 |     ```
244 | 
245 | 1. **Start the Server:**
246 | 
247 |     ```bash
248 |     hugo server
249 |     ```
250 | 
251 | ### Previewing Documentation on Pull Requests
252 | 
253 | ### Document Versioning Setup
254 | 
255 | There are 3 GHA workflows we use to achieve document versioning:
256 | 
257 | 1. **Deploy In-development docs:**
258 |     This workflow is run on every commit merged into the main branch. It deploys the built site to the `/dev/` subdirectory for the in-development documentation.
259 | 
260 | 1. **Deploy Versioned Docs:**
261 |     When a new GitHub Release is published, it performs two deployments based on the new release tag.
262 |     One to the new version subdirectory and one to the root directory of the versioned-gh-pages branch.
263 | 
264 |     **Note:** Before the release PR from release-please is merged, add the newest version into the hugo.toml file.
265 | 
266 | 1. **Deploy Previous Version Docs:**
267 |     This is a manual workflow, started from the GitHub Actions UI.
268 |     To rebuild and redeploy documentation for an already released version that were released before this new system was in place. This workflow can be started on the UI by providing the git version tag which you want to create the documentation for.
269 |     The specific versioned subdirectory and the root docs are updated on the versioned-gh-pages branch.
270 | 
271 | #### Contributors
272 | 
273 | Request a repo owner to run the preview deployment workflow on your PR. A
274 | preview link will be automatically added as a comment to your PR.
275 | 
276 | #### Maintainers
277 | 
278 | 1. **Inspect Changes:** Review the proposed changes in the PR to ensure they are
279 |    safe and do not contain malicious code. Pay close attention to changes in the
280 |    `.github/workflows/` directory.
281 | 1. **Deploy Preview:** Apply the `docs: deploy-preview` label to the PR to
282 |    deploy a documentation preview.
283 | 
284 | ## Building Toolbox
285 | 
286 | ### Building the Binary
287 | 
288 | 1. **Build Command:** Compile the Toolbox binary:
289 | 
290 |     ```bash
291 |     go build -o toolbox
292 |     ```
293 | 
294 | 1. **Running the Binary:** Execute the compiled binary with optional flags. The
295 |    server listens on port 5000 by default:
296 | 
297 |     ```bash
298 |     ./toolbox
299 |     ```
300 | 
301 | 1. **Testing the Endpoint:** Verify the server is running by sending a request
302 |    to the endpoint:
303 | 
304 |     ```bash
305 |     curl http://127.0.0.1:5000
306 |     ```
307 | 
308 | ### Building Container Images
309 | 
310 | 1. **Build Command:** Build the Toolbox container image:
311 | 
312 |     ```bash
313 |     docker build -t toolbox:dev .
314 |     ```
315 | 
316 | 1. **View Image:** List available Docker images to confirm the build:
317 | 
318 |     ```bash
319 |     docker images
320 |     ```
321 | 
322 | 1. **Run Container:** Run the Toolbox container image using Docker:
323 | 
324 |     ```bash
325 |     docker run -d toolbox:dev
326 |     ```
327 | 
328 | ## Developing Toolbox SDKs
329 | 
330 | Refer to the [SDK developer
331 | guide](https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/DEVELOPER.md)
332 | for instructions on developing Toolbox SDKs.
333 | 
334 | ## Maintainer Information
335 | 
336 | ### Team
337 | 
338 | Team `@googleapis/senseai-eco` has been set as
339 | [CODEOWNERS](.github/CODEOWNERS). The GitHub TeamSync tool is used to create
340 | this team from MDB Group, `senseai-eco`.
341 | 
342 | Team `@googleapis/toolbox-contributors` has write access to this repo. They
343 | can create branches and approve test runs. But they do not have the ability
344 | to approve PRs for main. TeamSync is used to create this team from the MDB
345 | Group `toolbox-contributors`. Googlers who are developing for MCP-Toolbox
346 | but aren't part of the core team should join this group.
347 | 
348 | ### Releasing
349 | 
350 | Toolbox has two types of releases: versioned and continuous. It uses Google
351 | Cloud project, `database-toolbox`.
352 | 
353 | * **Versioned Release:** Official, supported distributions tagged as `latest`.
354 |   The release process is defined in
355 |   [versioned.release.cloudbuild.yaml](.ci/versioned.release.cloudbuild.yaml).
356 | * **Continuous Release:** Used for early testing of features between official
357 |   releases and for end-to-end testing. The release process is defined in
358 |   [continuous.release.cloudbuild.yaml](.ci/continuous.release.cloudbuild.yaml).
359 | * **GitHub Release:** `.github/release-please.yml` automatically creates GitHub
360 |   Releases and release PRs.
361 | 
362 | ### How-to Release a new Version
363 | 
364 | 1. [Optional] If you want to override the version number, send a
365 |    [PR](https://github.com/googleapis/genai-toolbox/pull/31) to trigger
366 |    [release-please](https://github.com/googleapis/release-please?tab=readme-ov-file#how-do-i-change-the-version-number).
367 |    You can generate a commit with the following line: `git commit -m "chore:
368 |    release 0.1.0" -m "Release-As: 0.1.0" --allow-empty`
369 | 1. [Optional] If you want to edit the changelog, send commits to the release PR
370 | 1. Approve and merge the PR with the title “[chore(main): release
371 |    x.x.x](https://github.com/googleapis/genai-toolbox/pull/16)”
372 | 1. The
373 |    [trigger](https://pantheon.corp.google.com/cloud-build/triggers;region=us-central1/edit/27bd0d21-264a-4446-b2d7-0df4e9915fb3?e=13802955&inv=1&invt=AbhU8A&mods=logs_tg_staging&project=database-toolbox)
374 |    should automatically run when a new tag is pushed. You can view [triggered
375 |    builds here to check the
376 |    status](https://pantheon.corp.google.com/cloud-build/builds;region=us-central1?query=trigger_id%3D%2227bd0d21-264a-4446-b2d7-0df4e9915fb3%22&e=13802955&inv=1&invt=AbhU8A&mods=logs_tg_staging&project=database-toolbox)
377 | 1. Update the Github release notes to include the following table:
378 |     1. Run the following command (from the root directory):
379 | 
380 |         ```
381 |         export VERSION="v0.0.0"
382 |         .ci/generate_release_table.sh
383 |         ```
384 | 
385 |     1. Copy the table output
386 |     1. In the GitHub UI, navigate to Releases and click the `edit` button.
387 |     1. Paste the table at the bottom of release note and click `Update release`.
388 | 1. Post release in internal chat and on Discord.
389 | 
390 | #### Supported Binaries
391 | 
392 | The following operating systems and architectures are supported for binary
393 | releases:
394 | 
395 | * linux/amd64
396 | * darwin/arm64
397 | * darwin/amd64
398 | * windows/amd64
399 | 
400 | #### Supported Container Images
401 | 
402 | The following base container images are supported for container image releases:
403 | 
404 | * distroless
405 | 
406 | ### Automated Tests
407 | 
408 | Integration and unit tests are automatically triggered via Cloud Build on each
409 | pull request. Integration tests run on merge and nightly.
410 | 
411 | #### Failure notifications
412 | 
413 | On-merge and nightly tests that fail have notification setup via Cloud Build
414 | Failure Reporter [GitHub Actions
415 | Workflow](.github/workflows/schedule_reporter.yml).
416 | 
417 | #### Trigger Setup
418 | 
419 | Configure a Cloud Build trigger using the UI or `gcloud` with the following
420 | settings:
421 | 
422 | * **Event:** Pull request
423 | * **Region:** global (for default worker pools)
424 | * **Source:**
425 |   * Generation: 1st gen
426 |   * Repo: googleapis/genai-toolbox (GitHub App)
427 |   * Base branch: `^main$`
428 | * **Comment control:** Required except for owners and collaborators
429 | * **Filters:** Add directory filter
430 | * **Config:** Cloud Build configuration file
431 |   * Location: Repository (add path to file)
432 | * **Service account:** Set for demo service to enable ID token creation for
433 |   authenticated services
434 | 
435 | ### Triggering Tests
436 | 
437 | Trigger pull request tests for external contributors by:
438 | 
439 | * **Cloud Build tests:** Comment `/gcbrun`
440 | * **Unit tests:** Add the `tests:run` label
441 | 
442 | ## Repo Setup & Automation
443 | 
444 | * .github/blunderbuss.yml - Auto-assign issues and PRs from GitHub teams
445 | * .github/renovate.json5 - Tooling for dependency updates. Dependabot is built
446 |   into the GitHub repo for GitHub security warnings
447 | * go/github-issue-mirror - GitHub issues are automatically mirrored into buganizer
448 | * (Suspended) .github/sync-repo-settings.yaml - configure repo settings
449 | * .github/release-please.yml - Creates GitHub releases
450 | * .github/ISSUE_TEMPLATE - templates for GitHub issues
451 | 
```

--------------------------------------------------------------------------------
/internal/tools/firestore/firestorequerycollection/firestorequerycollection.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 firestorequerycollection
 16 | 
 17 | import (
 18 | 	"context"
 19 | 	"encoding/json"
 20 | 	"fmt"
 21 | 	"strings"
 22 | 
 23 | 	firestoreapi "cloud.google.com/go/firestore"
 24 | 	yaml "github.com/goccy/go-yaml"
 25 | 	"github.com/googleapis/genai-toolbox/internal/sources"
 26 | 	firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
 27 | 	"github.com/googleapis/genai-toolbox/internal/tools"
 28 | 	"github.com/googleapis/genai-toolbox/internal/tools/firestore/util"
 29 | )
 30 | 
 31 | // Constants for tool configuration
 32 | const (
 33 | 	kind            = "firestore-query-collection"
 34 | 	defaultLimit    = 100
 35 | 	defaultAnalyze  = false
 36 | 	maxFilterLength = 100 // Maximum filters to prevent abuse
 37 | )
 38 | 
 39 | // Parameter keys
 40 | const (
 41 | 	collectionPathKey = "collectionPath"
 42 | 	filtersKey        = "filters"
 43 | 	orderByKey        = "orderBy"
 44 | 	limitKey          = "limit"
 45 | 	analyzeQueryKey   = "analyzeQuery"
 46 | )
 47 | 
 48 | // Firestore operators
 49 | var validOperators = map[string]bool{
 50 | 	"<":                  true,
 51 | 	"<=":                 true,
 52 | 	">":                  true,
 53 | 	">=":                 true,
 54 | 	"==":                 true,
 55 | 	"!=":                 true,
 56 | 	"array-contains":     true,
 57 | 	"array-contains-any": true,
 58 | 	"in":                 true,
 59 | 	"not-in":             true,
 60 | }
 61 | 
 62 | // Error messages
 63 | const (
 64 | 	errMissingCollectionPath = "invalid or missing '%s' parameter"
 65 | 	errInvalidFilters        = "invalid '%s' parameter; expected an array"
 66 | 	errFilterNotString       = "filter at index %d is not a string"
 67 | 	errFilterParseFailed     = "failed to parse filter at index %d: %w"
 68 | 	errInvalidOperator       = "unsupported operator: %s. Valid operators are: %v"
 69 | 	errMissingFilterValue    = "no value specified for filter on field '%s'"
 70 | 	errOrderByParseFailed    = "failed to parse orderBy: %w"
 71 | 	errQueryExecutionFailed  = "failed to execute query: %w"
 72 | 	errTooManyFilters        = "too many filters provided: %d (maximum: %d)"
 73 | )
 74 | 
 75 | func init() {
 76 | 	if !tools.Register(kind, newConfig) {
 77 | 		panic(fmt.Sprintf("tool kind %q already registered", kind))
 78 | 	}
 79 | }
 80 | 
 81 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
 82 | 	actual := Config{Name: name}
 83 | 	if err := decoder.DecodeContext(ctx, &actual); err != nil {
 84 | 		return nil, err
 85 | 	}
 86 | 	return actual, nil
 87 | }
 88 | 
 89 | // compatibleSource defines the interface for sources that can provide a Firestore client
 90 | type compatibleSource interface {
 91 | 	FirestoreClient() *firestoreapi.Client
 92 | }
 93 | 
 94 | // validate compatible sources are still compatible
 95 | var _ compatibleSource = &firestoreds.Source{}
 96 | 
 97 | var compatibleSources = [...]string{firestoreds.SourceKind}
 98 | 
 99 | // Config represents the configuration for the Firestore query collection tool
100 | type Config struct {
101 | 	Name         string   `yaml:"name" validate:"required"`
102 | 	Kind         string   `yaml:"kind" validate:"required"`
103 | 	Source       string   `yaml:"source" validate:"required"`
104 | 	Description  string   `yaml:"description" validate:"required"`
105 | 	AuthRequired []string `yaml:"authRequired"`
106 | }
107 | 
108 | // validate interface
109 | var _ tools.ToolConfig = Config{}
110 | 
111 | // ToolConfigKind returns the kind of tool configuration
112 | func (cfg Config) ToolConfigKind() string {
113 | 	return kind
114 | }
115 | 
116 | // Initialize creates a new Tool instance from the configuration
117 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
118 | 	// verify source exists
119 | 	rawS, ok := srcs[cfg.Source]
120 | 	if !ok {
121 | 		return nil, fmt.Errorf("no source named %q configured", cfg.Source)
122 | 	}
123 | 
124 | 	// verify the source is compatible
125 | 	s, ok := rawS.(compatibleSource)
126 | 	if !ok {
127 | 		return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
128 | 	}
129 | 
130 | 	// Create parameters
131 | 	parameters := createParameters()
132 | 
133 | 	mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
134 | 
135 | 	// finish tool setup
136 | 	t := Tool{
137 | 		Name:         cfg.Name,
138 | 		Kind:         kind,
139 | 		Parameters:   parameters,
140 | 		AuthRequired: cfg.AuthRequired,
141 | 		Client:       s.FirestoreClient(),
142 | 		manifest:     tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
143 | 		mcpManifest:  mcpManifest,
144 | 	}
145 | 	return t, nil
146 | }
147 | 
148 | // createParameters creates the parameter definitions for the tool
149 | func createParameters() tools.Parameters {
150 | 	collectionPathParameter := tools.NewStringParameter(
151 | 		collectionPathKey,
152 | 		"The relative path to the Firestore collection to query (e.g., 'users' or 'users/userId/posts'). Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'",
153 | 	)
154 | 
155 | 	filtersDescription := `Array of filter objects to apply to the query. Each filter is a JSON string with:
156 | - field: The field name to filter on
157 | - op: The operator to use ("<", "<=", ">", ">=", "==", "!=", "array-contains", "array-contains-any", "in", "not-in")
158 | - value: The value to compare against (can be string, number, boolean, or array)
159 | Example: {"field": "age", "op": ">", "value": 18}`
160 | 
161 | 	filtersParameter := tools.NewArrayParameter(
162 | 		filtersKey,
163 | 		filtersDescription,
164 | 		tools.NewStringParameter("item", "JSON string representation of a filter object"),
165 | 	)
166 | 
167 | 	orderByParameter := tools.NewStringParameter(
168 | 		orderByKey,
169 | 		"JSON string specifying the field and direction to order by (e.g., {\"field\": \"name\", \"direction\": \"ASCENDING\"}). Leave empty if not specified",
170 | 	)
171 | 
172 | 	limitParameter := tools.NewIntParameterWithDefault(
173 | 		limitKey,
174 | 		defaultLimit,
175 | 		"The maximum number of documents to return",
176 | 	)
177 | 
178 | 	analyzeQueryParameter := tools.NewBooleanParameterWithDefault(
179 | 		analyzeQueryKey,
180 | 		defaultAnalyze,
181 | 		"If true, returns query explain metrics including execution statistics",
182 | 	)
183 | 
184 | 	return tools.Parameters{
185 | 		collectionPathParameter,
186 | 		filtersParameter,
187 | 		orderByParameter,
188 | 		limitParameter,
189 | 		analyzeQueryParameter,
190 | 	}
191 | }
192 | 
193 | // validate interface
194 | var _ tools.Tool = Tool{}
195 | 
196 | // Tool represents the Firestore query collection tool
197 | type Tool struct {
198 | 	Name         string           `yaml:"name"`
199 | 	Kind         string           `yaml:"kind"`
200 | 	AuthRequired []string         `yaml:"authRequired"`
201 | 	Parameters   tools.Parameters `yaml:"parameters"`
202 | 
203 | 	Client      *firestoreapi.Client
204 | 	manifest    tools.Manifest
205 | 	mcpManifest tools.McpManifest
206 | }
207 | 
208 | // FilterConfig represents a filter for the query
209 | type FilterConfig struct {
210 | 	Field string      `json:"field"`
211 | 	Op    string      `json:"op"`
212 | 	Value interface{} `json:"value"`
213 | }
214 | 
215 | // Validate checks if the filter configuration is valid
216 | func (f *FilterConfig) Validate() error {
217 | 	if f.Field == "" {
218 | 		return fmt.Errorf("filter field cannot be empty")
219 | 	}
220 | 
221 | 	if !validOperators[f.Op] {
222 | 		ops := make([]string, 0, len(validOperators))
223 | 		for op := range validOperators {
224 | 			ops = append(ops, op)
225 | 		}
226 | 		return fmt.Errorf(errInvalidOperator, f.Op, ops)
227 | 	}
228 | 
229 | 	if f.Value == nil {
230 | 		return fmt.Errorf(errMissingFilterValue, f.Field)
231 | 	}
232 | 
233 | 	return nil
234 | }
235 | 
236 | // OrderByConfig represents ordering configuration
237 | type OrderByConfig struct {
238 | 	Field     string `json:"field"`
239 | 	Direction string `json:"direction"`
240 | }
241 | 
242 | // GetDirection returns the Firestore direction constant
243 | func (o *OrderByConfig) GetDirection() firestoreapi.Direction {
244 | 	if strings.EqualFold(o.Direction, "DESCENDING") {
245 | 		return firestoreapi.Desc
246 | 	}
247 | 	return firestoreapi.Asc
248 | }
249 | 
250 | // QueryResult represents a document result from the query
251 | type QueryResult struct {
252 | 	ID         string         `json:"id"`
253 | 	Path       string         `json:"path"`
254 | 	Data       map[string]any `json:"data"`
255 | 	CreateTime interface{}    `json:"createTime,omitempty"`
256 | 	UpdateTime interface{}    `json:"updateTime,omitempty"`
257 | 	ReadTime   interface{}    `json:"readTime,omitempty"`
258 | }
259 | 
260 | // QueryResponse represents the full response including optional metrics
261 | type QueryResponse struct {
262 | 	Documents      []QueryResult  `json:"documents"`
263 | 	ExplainMetrics map[string]any `json:"explainMetrics,omitempty"`
264 | }
265 | 
266 | // Invoke executes the Firestore query based on the provided parameters
267 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
268 | 	// Parse parameters
269 | 	queryParams, err := t.parseQueryParameters(params)
270 | 	if err != nil {
271 | 		return nil, err
272 | 	}
273 | 
274 | 	// Build the query
275 | 	query, err := t.buildQuery(queryParams)
276 | 	if err != nil {
277 | 		return nil, err
278 | 	}
279 | 
280 | 	// Execute the query and return results
281 | 	return t.executeQuery(ctx, query, queryParams.AnalyzeQuery)
282 | }
283 | 
284 | // queryParameters holds all parsed query parameters
285 | type queryParameters struct {
286 | 	CollectionPath string
287 | 	Filters        []FilterConfig
288 | 	OrderBy        *OrderByConfig
289 | 	Limit          int
290 | 	AnalyzeQuery   bool
291 | }
292 | 
293 | // parseQueryParameters extracts and validates parameters from the input
294 | func (t Tool) parseQueryParameters(params tools.ParamValues) (*queryParameters, error) {
295 | 	mapParams := params.AsMap()
296 | 
297 | 	// Get collection path
298 | 	collectionPath, ok := mapParams[collectionPathKey].(string)
299 | 	if !ok || collectionPath == "" {
300 | 		return nil, fmt.Errorf(errMissingCollectionPath, collectionPathKey)
301 | 	}
302 | 
303 | 	// Validate collection path
304 | 	if err := util.ValidateCollectionPath(collectionPath); err != nil {
305 | 		return nil, fmt.Errorf("invalid collection path: %w", err)
306 | 	}
307 | 
308 | 	result := &queryParameters{
309 | 		CollectionPath: collectionPath,
310 | 		Limit:          defaultLimit,
311 | 		AnalyzeQuery:   defaultAnalyze,
312 | 	}
313 | 
314 | 	// Parse filters
315 | 	if filtersRaw, ok := mapParams[filtersKey]; ok && filtersRaw != nil {
316 | 		filters, err := t.parseFilters(filtersRaw)
317 | 		if err != nil {
318 | 			return nil, err
319 | 		}
320 | 		result.Filters = filters
321 | 	}
322 | 
323 | 	// Parse orderBy
324 | 	if orderByRaw, ok := mapParams[orderByKey]; ok && orderByRaw != nil {
325 | 		orderBy, err := t.parseOrderBy(orderByRaw)
326 | 		if err != nil {
327 | 			return nil, err
328 | 		}
329 | 		result.OrderBy = orderBy
330 | 	}
331 | 
332 | 	// Parse limit
333 | 	if limit, ok := mapParams[limitKey].(int); ok {
334 | 		result.Limit = limit
335 | 	}
336 | 
337 | 	// Parse analyze
338 | 	if analyze, ok := mapParams[analyzeQueryKey].(bool); ok {
339 | 		result.AnalyzeQuery = analyze
340 | 	}
341 | 
342 | 	return result, nil
343 | }
344 | 
345 | // parseFilters parses and validates filter configurations
346 | func (t Tool) parseFilters(filtersRaw interface{}) ([]FilterConfig, error) {
347 | 	filters, ok := filtersRaw.([]any)
348 | 	if !ok {
349 | 		return nil, fmt.Errorf(errInvalidFilters, filtersKey)
350 | 	}
351 | 
352 | 	if len(filters) > maxFilterLength {
353 | 		return nil, fmt.Errorf(errTooManyFilters, len(filters), maxFilterLength)
354 | 	}
355 | 
356 | 	result := make([]FilterConfig, 0, len(filters))
357 | 	for i, filterRaw := range filters {
358 | 		filterJSON, ok := filterRaw.(string)
359 | 		if !ok {
360 | 			return nil, fmt.Errorf(errFilterNotString, i)
361 | 		}
362 | 
363 | 		var filter FilterConfig
364 | 		if err := json.Unmarshal([]byte(filterJSON), &filter); err != nil {
365 | 			return nil, fmt.Errorf(errFilterParseFailed, i, err)
366 | 		}
367 | 
368 | 		if err := filter.Validate(); err != nil {
369 | 			return nil, fmt.Errorf("filter at index %d is invalid: %w", i, err)
370 | 		}
371 | 
372 | 		result = append(result, filter)
373 | 	}
374 | 
375 | 	return result, nil
376 | }
377 | 
378 | // parseOrderBy parses the orderBy configuration
379 | func (t Tool) parseOrderBy(orderByRaw interface{}) (*OrderByConfig, error) {
380 | 	orderByJSON, ok := orderByRaw.(string)
381 | 	if !ok || orderByJSON == "" {
382 | 		return nil, nil
383 | 	}
384 | 
385 | 	var orderBy OrderByConfig
386 | 	if err := json.Unmarshal([]byte(orderByJSON), &orderBy); err != nil {
387 | 		return nil, fmt.Errorf(errOrderByParseFailed, err)
388 | 	}
389 | 
390 | 	if orderBy.Field == "" {
391 | 		return nil, nil
392 | 	}
393 | 
394 | 	return &orderBy, nil
395 | }
396 | 
397 | // buildQuery constructs the Firestore query from parameters
398 | func (t Tool) buildQuery(params *queryParameters) (*firestoreapi.Query, error) {
399 | 	collection := t.Client.Collection(params.CollectionPath)
400 | 	query := collection.Query
401 | 
402 | 	// Apply filters
403 | 	if len(params.Filters) > 0 {
404 | 		filterConditions := make([]firestoreapi.EntityFilter, 0, len(params.Filters))
405 | 		for _, filter := range params.Filters {
406 | 			filterConditions = append(filterConditions, firestoreapi.PropertyFilter{
407 | 				Path:     filter.Field,
408 | 				Operator: filter.Op,
409 | 				Value:    filter.Value,
410 | 			})
411 | 		}
412 | 
413 | 		query = query.WhereEntity(firestoreapi.AndFilter{
414 | 			Filters: filterConditions,
415 | 		})
416 | 	}
417 | 
418 | 	// Apply ordering
419 | 	if params.OrderBy != nil {
420 | 		query = query.OrderBy(params.OrderBy.Field, params.OrderBy.GetDirection())
421 | 	}
422 | 
423 | 	// Apply limit
424 | 	query = query.Limit(params.Limit)
425 | 
426 | 	// Apply analyze options
427 | 	if params.AnalyzeQuery {
428 | 		query = query.WithRunOptions(firestoreapi.ExplainOptions{
429 | 			Analyze: true,
430 | 		})
431 | 	}
432 | 
433 | 	return &query, nil
434 | }
435 | 
436 | // executeQuery runs the query and formats the results
437 | func (t Tool) executeQuery(ctx context.Context, query *firestoreapi.Query, analyzeQuery bool) (any, error) {
438 | 	docIterator := query.Documents(ctx)
439 | 	docs, err := docIterator.GetAll()
440 | 	if err != nil {
441 | 		return nil, fmt.Errorf(errQueryExecutionFailed, err)
442 | 	}
443 | 
444 | 	// Convert results to structured format
445 | 	results := make([]QueryResult, len(docs))
446 | 	for i, doc := range docs {
447 | 		results[i] = QueryResult{
448 | 			ID:         doc.Ref.ID,
449 | 			Path:       doc.Ref.Path,
450 | 			Data:       doc.Data(),
451 | 			CreateTime: doc.CreateTime,
452 | 			UpdateTime: doc.UpdateTime,
453 | 			ReadTime:   doc.ReadTime,
454 | 		}
455 | 	}
456 | 
457 | 	// Return with explain metrics if requested
458 | 	if analyzeQuery {
459 | 		explainMetrics, err := t.getExplainMetrics(docIterator)
460 | 		if err == nil && explainMetrics != nil {
461 | 			response := QueryResponse{
462 | 				Documents:      results,
463 | 				ExplainMetrics: explainMetrics,
464 | 			}
465 | 			return response, nil
466 | 		}
467 | 	}
468 | 
469 | 	// Return just the documents
470 | 	resultsAny := make([]any, len(results))
471 | 	for i, r := range results {
472 | 		resultsAny[i] = r
473 | 	}
474 | 	return resultsAny, nil
475 | }
476 | 
477 | // getExplainMetrics extracts explain metrics from the query iterator
478 | func (t Tool) getExplainMetrics(docIterator *firestoreapi.DocumentIterator) (map[string]any, error) {
479 | 	explainMetrics, err := docIterator.ExplainMetrics()
480 | 	if err != nil || explainMetrics == nil {
481 | 		return nil, err
482 | 	}
483 | 
484 | 	metricsData := make(map[string]any)
485 | 
486 | 	// Add plan summary if available
487 | 	if explainMetrics.PlanSummary != nil {
488 | 		planSummary := make(map[string]any)
489 | 		planSummary["indexesUsed"] = explainMetrics.PlanSummary.IndexesUsed
490 | 		metricsData["planSummary"] = planSummary
491 | 	}
492 | 
493 | 	// Add execution stats if available
494 | 	if explainMetrics.ExecutionStats != nil {
495 | 		executionStats := make(map[string]any)
496 | 		executionStats["resultsReturned"] = explainMetrics.ExecutionStats.ResultsReturned
497 | 		executionStats["readOperations"] = explainMetrics.ExecutionStats.ReadOperations
498 | 
499 | 		if explainMetrics.ExecutionStats.ExecutionDuration != nil {
500 | 			executionStats["executionDuration"] = explainMetrics.ExecutionStats.ExecutionDuration.String()
501 | 		}
502 | 
503 | 		if explainMetrics.ExecutionStats.DebugStats != nil {
504 | 			executionStats["debugStats"] = *explainMetrics.ExecutionStats.DebugStats
505 | 		}
506 | 
507 | 		metricsData["executionStats"] = executionStats
508 | 	}
509 | 
510 | 	return metricsData, nil
511 | }
512 | 
513 | // ParseParams parses and validates input parameters
514 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
515 | 	return tools.ParseParams(t.Parameters, data, claims)
516 | }
517 | 
518 | // Manifest returns the tool manifest
519 | func (t Tool) Manifest() tools.Manifest {
520 | 	return t.manifest
521 | }
522 | 
523 | // McpManifest returns the MCP manifest
524 | func (t Tool) McpManifest() tools.McpManifest {
525 | 	return t.mcpManifest
526 | }
527 | 
528 | // Authorized checks if the tool is authorized based on verified auth services
529 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
530 | 	return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
531 | }
532 | 
533 | func (t Tool) RequiresClientAuthorization() bool {
534 | 	return false
535 | }
536 | 
```

--------------------------------------------------------------------------------
/internal/prebuiltconfigs/tools/cloud-sql-mysql-observability.yaml:
--------------------------------------------------------------------------------

```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 | sources:
 15 |   cloud-monitoring-source:
 16 |     kind: cloud-monitoring
 17 | tools:
 18 |   get_system_metrics:
 19 |     kind: cloud-monitoring-query-prometheus
 20 |     source: cloud-monitoring-source
 21 |     description: |
 22 |       Fetches system level cloudmonitoring data (timeseries metrics) for a MySQL instance using a PromQL query. Take projectId and instanceId from the user for which the metrics timeseries data needs to be fetched.
 23 |       To use this tool, you must provide the Google Cloud `projectId` and a PromQL `query`.
 24 | 
 25 |       Generate PromQL `query` for MySQL system metrics. Use the provided metrics and rules to construct queries, Get the labels like `instance_id` from user intent.
 26 | 
 27 |       Defaults:
 28 |       1. Interval: Use a default interval of `5m` for `_over_time` aggregation functions unless a different window is specified by the user.
 29 | 
 30 |       PromQL Query Examples:
 31 |       1. Basic Time Series: `avg_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m])`
 32 |       2. Top K: `topk(30, avg_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 33 |       3. Mean: `avg(avg_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 34 |       4. Minimum: `min(min_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 35 |       5. Maximum: `max(max_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 36 |       6. Sum: `sum(avg_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 37 |       7. Count streams: `count(avg_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 38 |       8. Percentile with groupby on database_id: `quantile by ("database_id")(0.99,avg_over_time({"__name__"="cloudsql.googleapis.com/database/cpu/utilization","monitored_resource"="cloudsql_database","project_id"="my-projectId","database_id"="my-projectId:my-instanceId"}[5m]))`
 39 | 
 40 |       Available Metrics List: metricname. description. monitored resource. labels. database_id is actually the instance id and the format is `project_id:instance_id`. 
 41 |       1. `cloudsql.googleapis.com/database/cpu/utilization`: Current CPU utilization as a percentage of reserved CPU. `cloudsql_database`. `database`, `project_id`, `database_id`.
 42 |       2. `cloudsql.googleapis.com/database/network/connections`: Number of connections to the database instance. `cloudsql_database`. `database`, `project_id`, `database_id`.
 43 |       3. `cloudsql.googleapis.com/database/network/received_bytes_count`: Delta count of bytes received through the network. `cloudsql_database`. `database`, `project_id`, `database_id`.
 44 |       4. `cloudsql.googleapis.com/database/network/sent_bytes_count`: Delta count of bytes sent through the network. `cloudsql_database`. `destination`, `database`, `project_id`, `database_id`.
 45 |       5. `cloudsql.googleapis.com/database/memory/components`: Memory usage for components like usage, cache, and free memory. `cloudsql_database`. `component`, `database`, `project_id`, `database_id`.
 46 |       6. `cloudsql.googleapis.com/database/disk/bytes_used_by_data_type`: Data utilization in bytes. `cloudsql_database`. `data_type`, `database`, `project_id`, `database_id`.
 47 |       7. `cloudsql.googleapis.com/database/disk/read_ops_count`: Delta count of data disk read IO operations. `cloudsql_database`. `database`, `project_id`, `database_id`.
 48 |       8. `cloudsql.googleapis.com/database/disk/write_ops_count`: Delta count of data disk write IO operations. `cloudsql_database`. `database`, `project_id`, `database_id`.
 49 |       9. `cloudsql.googleapis.com/database/mysql/queries`: Delta count of statements executed by the server. `cloudsql_database`. `database`, `project_id`, `database_id`.
 50 |       10. `cloudsql.googleapis.com/database/mysql/questions`: Delta count of statements sent by the client. `cloudsql_database`. `database`, `project_id`, `database_id`.
 51 |       11. `cloudsql.googleapis.com/database/mysql/received_bytes_count`: Delta count of bytes received by MySQL process. `cloudsql_database`. `database`, `project_id`, `database_id`.
 52 |       12. `cloudsql.googleapis.com/database/mysql/sent_bytes_count`: Delta count of bytes sent by MySQL process. `cloudsql_database`. `database`, `project_id`, `database_id`.
 53 |       13. `cloudsql.googleapis.com/database/mysql/innodb_buffer_pool_pages_dirty`: Number of unflushed pages in the InnoDB buffer pool. `cloudsql_database`. `database`, `project_id`, `database_id`.
 54 |       14. `cloudsql.googleapis.com/database/mysql/innodb_buffer_pool_pages_free`: Number of unused pages in the InnoDB buffer pool. `cloudsql_database`. `database`, `project_id`, `database_id`.
 55 |       15. `cloudsql.googleapis.com/database/mysql/innodb_buffer_pool_pages_total`: Total number of pages in the InnoDB buffer pool. `cloudsql_database`. `database`, `project_id`, `database_id`.
 56 |       16. `cloudsql.googleapis.com/database/mysql/innodb_data_fsyncs`: Delta count of InnoDB fsync() calls. `cloudsql_database`. `database`, `project_id`, `database_id`.
 57 |       17. `cloudsql.googleapis.com/database/mysql/innodb_os_log_fsyncs`: Delta count of InnoDB fsync() calls to the log file. `cloudsql_database`. `database`, `project_id`, `database_id`.
 58 |       18. `cloudsql.googleapis.com/database/mysql/innodb_pages_read`: Delta count of InnoDB pages read. `cloudsql_database`. `database`, `project_id`, `database_id`.
 59 |       19. `cloudsql.googleapis.com/database/mysql/innodb_pages_written`: Delta count of InnoDB pages written. `cloudsql_database`. `database`, `project_id`, `database_id`.
 60 |       20. `cloudsql.googleapis.com/database/mysql/open_tables`: The number of tables that are currently open. `cloudsql_database`. `database`, `project_id`, `database_id`.
 61 |       21. `cloudsql.googleapis.com/database/mysql/opened_table_count`: The number of tables opened since the last sample. `cloudsql_database`. `database`, `project_id`, `database_id`.
 62 |       22. `cloudsql.googleapis.com/database/mysql/open_table_definitions`: The number of table definitions currently cached. `cloudsql_database`. `database`, `project_id`, `database_id`.
 63 |       23. `cloudsql.googleapis.com/database/mysql/opened_table_definitions_count`: The number of table definitions cached since the last sample. `cloudsql_database`. `database`, `project_id`, `database_id`.
 64 |       24. `cloudsql.googleapis.com/database/mysql/innodb/dictionary_memory`: Memory allocated for the InnoDB dictionary cache. `cloudsql_database`. `database`, `project_id`, `database_id`.
 65 | 
 66 |   get_query_metrics:
 67 |     kind: cloud-monitoring-query-prometheus
 68 |     source: cloud-monitoring-source
 69 |     description: |
 70 |       Fetches query level cloudmonitoring data (timeseries metrics) for queries running in Mysql instance using a PromQL query. Take projectID and instanceID from the user for which the metrics timeseries data needs to be fetched.
 71 |       To use this tool, you must provide the Google Cloud `projectId` and a PromQL `query`.
 72 | 
 73 |       Generate PromQL `query` for Mysql query metrics. Use the provided metrics and rules to construct queries, Get the labels like `instance_id`, `query_hash` from user intent. If query_hash is provided then use the per_query metrics. Query hash and query id are same.
 74 | 
 75 |       Defaults:
 76 |       1. Interval: Use a default interval of `5m` for `_over_time` aggregation functions unless a different window is specified by the user.
 77 | 
 78 |       PromQL Query Examples:
 79 |       1. Basic Time Series: `avg_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m])`
 80 |       2. Top K: `topk(30, avg_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 81 |       3. Mean: `avg(avg_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 82 |       4. Minimum: `min(min_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 83 |       5. Maximum: `max(max_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 84 |       6. Sum: `sum(avg_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 85 |       7. Count streams: `count(avg_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 86 |       8. Percentile with groupby on resource_id, database: `quantile by ("resource_id","database")(0.99,avg_over_time({"__name__"="dbinsights.googleapis.com/aggregate/execution_time","monitored_resource"="cloudsql_instance_database","project_id"="my-projectId","resource_id"="my-projectId:my-instanceId"}[5m]))`
 87 | 
 88 |       Available Metrics List: metricname. description. monitored resource. labels. resource_id label format is `project_id:instance_id` which is actually instance id only. aggregate is the aggregated values for all query stats, Use aggregate metrics if query id is not provided. For perquery metrics do not fetch querystring unless specified by user specifically. Have the aggregation on query hash to avoid fetching the querystring. Do not use latency metrics for anything.
 89 |       1. `dbinsights.googleapis.com/aggregate/latencies`: Cumulative query latency distribution per user and database. `cloudsql_instance_database`. `user`, `client_addr`, `database`, `project_id`, `resource_id`.
 90 |       2. `dbinsights.googleapis.com/aggregate/execution_time`: Cumulative query execution time per user and database. `cloudsql_instance_database`. `user`, `client_addr`, `database`, `project_id`, `resource_id`.
 91 |       3. `dbinsights.googleapis.com/aggregate/execution_count`: Total number of query executions per user and database. `cloudsql_instance_database`. `user`, `client_addr`, `database`, `project_id`, `resource_id`.
 92 |       4. `dbinsights.googleapis.com/aggregate/lock_time`: Cumulative lock wait time per user and database. `cloudsql_instance_database`. `user`, `client_addr`, `lock_type`, `database`, `project_id`, `resource_id`.
 93 |       5. `dbinsights.googleapis.com/aggregate/io_time`: Cumulative IO wait time per user and database. `cloudsql_instance_database`. `user`, `client_addr`, `database`, `project_id`, `resource_id`.
 94 |       6. `dbinsights.googleapis.com/aggregate/row_count`: Total number of rows affected during query execution. `cloudsql_instance_database`. `user`, `client_addr`, `row_status`, `database`, `project_id`, `resource_id`.
 95 |       7. `dbinsights.googleapis.com/perquery/latencies`: Cumulative query latency distribution per user, database, and query. `cloudsql_instance_database`. `querystring`, `user`, `client_addr`, `query_hash`, `database`, `project_id`, `resource_id`.
 96 |       8. `dbinsights.googleapis.com/perquery/execution_time`: Cumulative query execution time per user, database, and query. `cloudsql_instance_database`. `querystring`, `user`, `client_addr`, `query_hash`, `database`, `project_id`, `resource_id`.
 97 |       9. `dbinsights.googleapis.com/perquery/execution_count`: Total number of query executions per user, database, and query. `cloudsql_instance_database`. `querystring`, `user`, `client_addr`, `query_hash`, `database`, `project_id`, `resource_id`.
 98 |       10. `dbinsights.googleapis.com/perquery/lock_time`: Cumulative lock wait time per user, database, and query. `cloudsql_instance_database`. `querystring`, `user`, `client_addr`, `lock_type`, `query_hash`, `database`, `project_id`, `resource_id`.
 99 |       11. `dbinsights.googleapis.com/perquery/io_time`: Cumulative io wait time per user, database, and query. `cloudsql_instance_database`. `querystring`, `user`, `client_addr`, `query_hash`, `database`, `project_id`, `resource_id`.
100 |       12. `dbinsights.googleapis.com/perquery/row_count`: Total number of rows affected during query execution. `cloudsql_instance_database`. `querystring`, `user`, `client_addr`, `query_hash`, `row_status`, `database`, `project_id`, `resource_id`.
101 |       13. `dbinsights.googleapis.com/pertag/latencies`: Cumulative query latency distribution per user, database, and tag. `cloudsql_instance_database`. `user`, `client_addr`, `action`, `application`, `controller`, `db_driver`, `framework`, `route`, `tag_hash`, `database`, `project_id`, `resource_id`.
102 |       14. `dbinsights.googleapis.com/pertag/execution_time`: Cumulative query execution time per user, database, and tag. `cloudsql_instance_database`. `user`, `client_addr`, `action`, `application`, `controller`, `db_driver`, `framework`, `route`, `tag_hash`, `database`, `project_id`, `resource_id`.
103 |       15. `dbinsights.googleapis.com/pertag/execution_count`: Total number of query executions per user, database, and tag. `cloudsql_instance_database`. `user`, `client_addr`, `action`, `application`, `controller`, `db_driver`, `framework`, `route`, `tag_hash`, `database`, `project_id`, `resource_id`.
104 |       16. `dbinsights.googleapis.com/pertag/lock_time`: Cumulative lock wait time per user, database and tag. `cloudsql_instance_database`. `user`, `client_addr`, `action`, `application`, `controller`, `db_driver`, `framework`, `route`, `lock_type`, `tag_hash`, `database`, `project_id`, `resource_id`.
105 |       17. `dbinsights.googleapis.com/pertag/io_time`: Cumulative IO wait time per user, database and tag. `cloudsql_instance_database`. `user`, `client_addr`, `action`, `application`, `controller`, `db_driver`, `framework`, `route`, `tag_hash`, `database`, `project_id`, `resource_id`.
106 |       18. `dbinsights.googleapis.com/pertag/row_count`: Total number of rows affected during query execution. `cloudsql_instance_database`. `user`, `client_addr`, `action`, `application`, `controller`, `db_driver`, `framework`, `route`, `tag_hash`, `row_status`, `database`, `project_id`, `resource_id`.
107 | 
108 | toolsets:
109 |   cloud_sql_mysql_cloud_monitoring_tools:
110 |     - get_system_metrics
111 |     - get_query_metrics
```

