This is page 57 of 76. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .ci
│ ├── continuous.release.cloudbuild.yaml
│ ├── generate_release_table.sh
│ ├── integration.cloudbuild.yaml
│ ├── quickstart_test
│ │ ├── go.integration.cloudbuild.yaml
│ │ ├── js.integration.cloudbuild.yaml
│ │ ├── py.integration.cloudbuild.yaml
│ │ ├── run_go_tests.sh
│ │ ├── run_js_tests.sh
│ │ ├── run_py_tests.sh
│ │ └── setup_hotels_sample.sql
│ ├── test_prompts_with_coverage.sh
│ ├── test_with_coverage.sh
│ └── versioned.release.cloudbuild.yaml
├── .gemini
│ └── config.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
│ ├── trusted-contribution.yml
│ └── workflows
│ ├── cloud_build_failure_reporter.yml
│ ├── deploy_dev_docs.yaml
│ ├── deploy_previous_version_docs.yaml
│ ├── deploy_versioned_docs.yaml
│ ├── docs_preview_clean.yaml
│ ├── docs_preview_deploy.yaml
│ ├── link_checker_workflow.yaml
│ ├── lint.yaml
│ ├── publish-mcp.yml
│ ├── 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
├── .lycheeignore
├── 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
│ ├── ALLOYDBADMIN_README.md
│ ├── ALLOYDBPG_README.md
│ ├── BIGQUERY_README.md
│ ├── CLOUDSQLMSSQL_README.md
│ ├── CLOUDSQLMSSQLADMIN_README.md
│ ├── CLOUDSQLMYSQL_README.md
│ ├── CLOUDSQLMYSQLADMIN_README.md
│ ├── CLOUDSQLPG_README.md
│ ├── CLOUDSQLPGADMIN_README.md
│ ├── DATAPLEX_README.md
│ ├── en
│ │ ├── _index.md
│ │ ├── about
│ │ │ ├── _index.md
│ │ │ └── faq.md
│ │ ├── blogs
│ │ │ └── _index.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
│ │ │ ├── prompts_quickstart_gemini_cli.md
│ │ │ └── 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
│ │ │ │ ├── adk
│ │ │ │ │ ├── package-lock.json
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── quickstart.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_adk_agent.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
│ │ │ ├── embeddingModels
│ │ │ │ ├── _index.md
│ │ │ │ └── gemini.md
│ │ │ ├── prompts
│ │ │ │ ├── _index.md
│ │ │ │ └── custom
│ │ │ │ └── _index.md
│ │ │ ├── sources
│ │ │ │ ├── _index.md
│ │ │ │ ├── alloydb-admin.md
│ │ │ │ ├── alloydb-pg.md
│ │ │ │ ├── bigquery.md
│ │ │ │ ├── bigtable.md
│ │ │ │ ├── cassandra.md
│ │ │ │ ├── clickhouse.md
│ │ │ │ ├── cloud-gda.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
│ │ │ │ ├── mariadb.md
│ │ │ │ ├── mindsdb.md
│ │ │ │ ├── mongodb.md
│ │ │ │ ├── mssql.md
│ │ │ │ ├── mysql.md
│ │ │ │ ├── neo4j.md
│ │ │ │ ├── oceanbase.md
│ │ │ │ ├── oracle.md
│ │ │ │ ├── postgres.md
│ │ │ │ ├── redis.md
│ │ │ │ ├── serverless-spark.md
│ │ │ │ ├── singlestore.md
│ │ │ │ ├── snowflake.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
│ │ │ ├── cloudgda
│ │ │ │ ├── _index.md
│ │ │ │ └── cloud-gda-query.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
│ │ │ │ ├── cloudsqlcloneinstance.md
│ │ │ │ ├── cloudsqlcreatedatabase.md
│ │ │ │ ├── cloudsqlcreateusers.md
│ │ │ │ ├── cloudsqlgetinstances.md
│ │ │ │ ├── cloudsqllistdatabases.md
│ │ │ │ ├── cloudsqllistinstances.md
│ │ │ │ ├── cloudsqlmssqlcreateinstance.md
│ │ │ │ ├── cloudsqlmysqlcreateinstance.md
│ │ │ │ ├── cloudsqlpgcreateinstances.md
│ │ │ │ ├── cloudsqlpgupgradeprecheck.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-add-dashboard-filter.md
│ │ │ │ ├── looker-conversational-analytics.md
│ │ │ │ ├── looker-create-project-file.md
│ │ │ │ ├── looker-delete-project-file.md
│ │ │ │ ├── looker-dev-mode.md
│ │ │ │ ├── looker-generate-embed-url.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-get-query-plan.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-database-overview.md
│ │ │ │ ├── postgres-execute-sql.md
│ │ │ │ ├── postgres-get-column-cardinality.md
│ │ │ │ ├── postgres-list-active-queries.md
│ │ │ │ ├── postgres-list-available-extensions.md
│ │ │ │ ├── postgres-list-database-stats.md
│ │ │ │ ├── postgres-list-indexes.md
│ │ │ │ ├── postgres-list-installed-extensions.md
│ │ │ │ ├── postgres-list-locks.md
│ │ │ │ ├── postgres-list-pg-settings.md
│ │ │ │ ├── postgres-list-publication-tables.md
│ │ │ │ ├── postgres-list-query-stats.md
│ │ │ │ ├── postgres-list-roles.md
│ │ │ │ ├── postgres-list-schemas.md
│ │ │ │ ├── postgres-list-sequences.md
│ │ │ │ ├── postgres-list-stored-procedure.md
│ │ │ │ ├── postgres-list-table-stats.md
│ │ │ │ ├── postgres-list-tables.md
│ │ │ │ ├── postgres-list-tablespaces.md
│ │ │ │ ├── postgres-list-triggers.md
│ │ │ │ ├── postgres-list-views.md
│ │ │ │ ├── postgres-long-running-transactions.md
│ │ │ │ ├── postgres-replication-stats.md
│ │ │ │ └── postgres-sql.md
│ │ │ ├── redis
│ │ │ │ ├── _index.md
│ │ │ │ └── redis.md
│ │ │ ├── serverless-spark
│ │ │ │ ├── _index.md
│ │ │ │ ├── serverless-spark-cancel-batch.md
│ │ │ │ ├── serverless-spark-create-pyspark-batch.md
│ │ │ │ ├── serverless-spark-create-spark-batch.md
│ │ │ │ ├── serverless-spark-get-batch.md
│ │ │ │ └── serverless-spark-list-batches.md
│ │ │ ├── singlestore
│ │ │ │ ├── _index.md
│ │ │ │ ├── singlestore-execute-sql.md
│ │ │ │ └── singlestore-sql.md
│ │ │ ├── snowflake
│ │ │ │ ├── _index.md
│ │ │ │ ├── snowflake-execute-sql.md
│ │ │ │ └── snowflake-sql.md
│ │ │ ├── spanner
│ │ │ │ ├── _index.md
│ │ │ │ ├── spanner-execute-sql.md
│ │ │ │ ├── spanner-list-graphs.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
│ │ │ └── snowflake
│ │ │ ├── _index.md
│ │ │ ├── runme.py
│ │ │ ├── snowflake-config.yaml
│ │ │ ├── snowflake-env.sh
│ │ │ └── test-snowflake.sh
│ │ └── sdks
│ │ ├── _index.md
│ │ ├── go-sdk.md
│ │ ├── js-sdk.md
│ │ └── python-sdk.md
│ ├── LOOKER_README.md
│ ├── SPANNER_README.md
│ └── TOOLBOX_README.md
├── gemini-extension.json
├── go.mod
├── go.sum
├── internal
│ ├── auth
│ │ ├── auth.go
│ │ └── google
│ │ └── google.go
│ ├── embeddingmodels
│ │ ├── embeddingmodels.go
│ │ └── gemini
│ │ ├── gemini_test.go
│ │ └── gemini.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
│ │ ├── snowflake.yaml
│ │ ├── spanner-postgres.yaml
│ │ ├── spanner.yaml
│ │ └── sqlite.yaml
│ ├── prompts
│ │ ├── arguments_test.go
│ │ ├── arguments.go
│ │ ├── custom
│ │ │ ├── custom_test.go
│ │ │ └── custom.go
│ │ ├── messages_test.go
│ │ ├── messages.go
│ │ ├── prompts_test.go
│ │ ├── prompts.go
│ │ ├── promptsets_test.go
│ │ └── promptsets.go
│ ├── 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
│ │ ├── resources
│ │ │ ├── resources_test.go
│ │ │ └── resources.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
│ │ ├── cloudgda
│ │ │ ├── cloud_gda_test.go
│ │ │ └── cloud_gda.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_test.go
│ │ │ └── oracle.go
│ │ ├── postgres
│ │ │ ├── postgres_test.go
│ │ │ └── postgres.go
│ │ ├── redis
│ │ │ ├── redis_test.go
│ │ │ └── redis.go
│ │ ├── serverlessspark
│ │ │ ├── serverlessspark_test.go
│ │ │ ├── serverlessspark.go
│ │ │ ├── url_test.go
│ │ │ └── url.go
│ │ ├── singlestore
│ │ │ ├── singlestore_test.go
│ │ │ └── singlestore.go
│ │ ├── snowflake
│ │ │ ├── snowflake_test.go
│ │ │ └── snowflake.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
│ │ ├── cloudgda
│ │ │ ├── cloudgda_test.go
│ │ │ ├── cloudgda.go
│ │ │ └── types.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
│ │ │ ├── cloudsqlcloneinstance
│ │ │ │ ├── cloudsqlcloneinstance_test.go
│ │ │ │ └── cloudsqlcloneinstance.go
│ │ │ ├── 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
│ │ │ └── cloudsqlpgupgradeprecheck
│ │ │ ├── cloudsqlpgupgradeprecheck_test.go
│ │ │ └── cloudsqlpgupgradeprecheck.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
│ │ │ ├── lookeradddashboardfilter
│ │ │ │ ├── lookeradddashboardfilter_test.go
│ │ │ │ └── lookeradddashboardfilter.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
│ │ │ ├── lookergenerateembedurl
│ │ │ │ ├── lookergenerateembedurl_test.go
│ │ │ │ └── lookergenerateembedurl.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
│ │ │ ├── mysqlgetqueryplan
│ │ │ │ ├── mysqlgetqueryplan_test.go
│ │ │ │ └── mysqlgetqueryplan.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_test.go
│ │ │ │ └── oracleexecutesql.go
│ │ │ └── oraclesql
│ │ │ ├── oraclesql_test.go
│ │ │ └── oraclesql.go
│ │ ├── postgres
│ │ │ ├── postgresdatabaseoverview
│ │ │ │ ├── postgresdatabaseoverview_test.go
│ │ │ │ └── postgresdatabaseoverview.go
│ │ │ ├── postgresexecutesql
│ │ │ │ ├── postgresexecutesql_test.go
│ │ │ │ └── postgresexecutesql.go
│ │ │ ├── postgresgetcolumncardinality
│ │ │ │ ├── postgresgetcolumncardinality_test.go
│ │ │ │ └── postgresgetcolumncardinality.go
│ │ │ ├── postgreslistactivequeries
│ │ │ │ ├── postgreslistactivequeries_test.go
│ │ │ │ └── postgreslistactivequeries.go
│ │ │ ├── postgreslistavailableextensions
│ │ │ │ ├── postgreslistavailableextensions_test.go
│ │ │ │ └── postgreslistavailableextensions.go
│ │ │ ├── postgreslistdatabasestats
│ │ │ │ ├── postgreslistdatabasestats_test.go
│ │ │ │ └── postgreslistdatabasestats.go
│ │ │ ├── postgreslistindexes
│ │ │ │ ├── postgreslistindexes_test.go
│ │ │ │ └── postgreslistindexes.go
│ │ │ ├── postgreslistinstalledextensions
│ │ │ │ ├── postgreslistinstalledextensions_test.go
│ │ │ │ └── postgreslistinstalledextensions.go
│ │ │ ├── postgreslistlocks
│ │ │ │ ├── postgreslistlocks_test.go
│ │ │ │ └── postgreslistlocks.go
│ │ │ ├── postgreslistpgsettings
│ │ │ │ ├── postgreslistpgsettings_test.go
│ │ │ │ └── postgreslistpgsettings.go
│ │ │ ├── postgreslistpublicationtables
│ │ │ │ ├── postgreslistpublicationtables_test.go
│ │ │ │ └── postgreslistpublicationtables.go
│ │ │ ├── postgreslistquerystats
│ │ │ │ ├── postgreslistquerystats_test.go
│ │ │ │ └── postgreslistquerystats.go
│ │ │ ├── postgreslistroles
│ │ │ │ ├── postgreslistroles_test.go
│ │ │ │ └── postgreslistroles.go
│ │ │ ├── postgreslistschemas
│ │ │ │ ├── postgreslistschemas_test.go
│ │ │ │ └── postgreslistschemas.go
│ │ │ ├── postgreslistsequences
│ │ │ │ ├── postgreslistsequences_test.go
│ │ │ │ └── postgreslistsequences.go
│ │ │ ├── postgresliststoredprocedure
│ │ │ │ ├── postgresliststoredprocedure_test.go
│ │ │ │ └── postgresliststoredprocedure.go
│ │ │ ├── postgreslisttables
│ │ │ │ ├── postgreslisttables_test.go
│ │ │ │ └── postgreslisttables.go
│ │ │ ├── postgreslisttablespaces
│ │ │ │ ├── postgreslisttablespaces_test.go
│ │ │ │ └── postgreslisttablespaces.go
│ │ │ ├── postgreslisttablestats
│ │ │ │ ├── postgreslisttablestats_test.go
│ │ │ │ └── postgreslisttablestats.go
│ │ │ ├── postgreslisttriggers
│ │ │ │ ├── postgreslisttriggers_test.go
│ │ │ │ └── postgreslisttriggers.go
│ │ │ ├── postgreslistviews
│ │ │ │ ├── postgreslistviews_test.go
│ │ │ │ └── postgreslistviews.go
│ │ │ ├── postgreslongrunningtransactions
│ │ │ │ ├── postgreslongrunningtransactions_test.go
│ │ │ │ └── postgreslongrunningtransactions.go
│ │ │ ├── postgresreplicationstats
│ │ │ │ ├── postgresreplicationstats_test.go
│ │ │ │ └── postgresreplicationstats.go
│ │ │ └── postgressql
│ │ │ ├── postgressql_test.go
│ │ │ └── postgressql.go
│ │ ├── redis
│ │ │ ├── redis_test.go
│ │ │ └── redis.go
│ │ ├── serverlessspark
│ │ │ ├── createbatch
│ │ │ │ ├── config.go
│ │ │ │ └── tool.go
│ │ │ ├── serverlesssparkcancelbatch
│ │ │ │ ├── serverlesssparkcancelbatch_test.go
│ │ │ │ └── serverlesssparkcancelbatch.go
│ │ │ ├── serverlesssparkcreatepysparkbatch
│ │ │ │ ├── serverlesssparkcreatepysparkbatch_test.go
│ │ │ │ └── serverlesssparkcreatepysparkbatch.go
│ │ │ ├── serverlesssparkcreatesparkbatch
│ │ │ │ ├── serverlesssparkcreatesparkbatch_test.go
│ │ │ │ └── serverlesssparkcreatesparkbatch.go
│ │ │ ├── serverlesssparkgetbatch
│ │ │ │ ├── serverlesssparkgetbatch_test.go
│ │ │ │ └── serverlesssparkgetbatch.go
│ │ │ ├── serverlesssparklistbatches
│ │ │ │ ├── serverlesssparklistbatches_test.go
│ │ │ │ └── serverlesssparklistbatches.go
│ │ │ └── testutils
│ │ │ └── testutils.go
│ │ ├── singlestore
│ │ │ ├── singlestoreexecutesql
│ │ │ │ ├── singlestoreexecutesql_test.go
│ │ │ │ └── singlestoreexecutesql.go
│ │ │ └── singlestoresql
│ │ │ ├── singlestoresql_test.go
│ │ │ └── singlestoresql.go
│ │ ├── snowflake
│ │ │ ├── snowflakeexecutesql
│ │ │ │ ├── snowflakeexecutesql_test.go
│ │ │ │ └── snowflakeexecutesql.go
│ │ │ └── snowflakesql
│ │ │ ├── snowflakesql_test.go
│ │ │ └── snowflakesql.go
│ │ ├── spanner
│ │ │ ├── spannerexecutesql
│ │ │ │ ├── spannerexecutesql_test.go
│ │ │ │ └── spannerexecutesql.go
│ │ │ ├── spannerlistgraphs
│ │ │ │ ├── spannerlistgraphs_test.go
│ │ │ │ └── spannerlistgraphs.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
│ ├── parameters
│ │ ├── common_test.go
│ │ ├── common.go
│ │ ├── parameters_test.go
│ │ └── parameters.go
│ └── util.go
├── LICENSE
├── logo.png
├── main.go
├── MCP-TOOLBOX-EXTENSION.md
├── README.md
├── server.json
└── 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
├── cloudgda
│ └── cloud_gda_integration_test.go
├── cloudhealthcare
│ └── cloud_healthcare_integration_test.go
├── cloudmonitoring
│ └── cloud_monitoring_integration_test.go
├── cloudsql
│ ├── cloud_sql_clone_instance_test.go
│ ├── 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
│ └── cloud_sql_pg_upgrade_precheck_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
├── mariadb
│ └── mariadb_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
├── prompts
│ └── custom
│ └── prompts_integration_test.go
├── redis
│ └── redis_test.go
├── server.go
├── serverlessspark
│ └── serverless_spark_integration_test.go
├── singlestore
│ └── singlestore_integration_test.go
├── snowflake
│ └── snowflake_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
--------------------------------------------------------------------------------
/docs/en/resources/tools/looker/looker-query-url.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: "looker-query-url"
3 | type: docs
4 | weight: 1
5 | description: >
6 | "looker-query-url" generates a url link to a Looker explore.
7 | aliases:
8 | - /resources/tools/looker-query-url
9 | ---
10 |
11 | ## About
12 |
13 | The `looker-query-url` generates a url link to an explore in
14 | Looker so the query can be investigated further.
15 |
16 | It's compatible with the following sources:
17 |
18 | - [looker](../../sources/looker.md)
19 |
20 | `looker-query-url` takes nine parameters:
21 |
22 | 1. the `model`
23 | 2. the `explore`
24 | 3. the `fields` list
25 | 4. an optional set of `filters`
26 | 5. an optional set of `pivots`
27 | 6. an optional set of `sorts`
28 | 7. an optional `limit`
29 | 8. an optional `tz`
30 | 9. an optional `vis_config`
31 |
32 | ## Example
33 |
34 | ```yaml
35 | tools:
36 | query_url:
37 | kind: looker-query-url
38 | source: looker-source
39 | description: |
40 | This tool generates a shareable URL for a Looker query, allowing users to
41 | explore the query further within the Looker UI. It returns the generated URL,
42 | along with the `query_id` and `slug`.
43 |
44 | Parameters:
45 | All query parameters (e.g., `model_name`, `explore_name`, `fields`, `pivots`,
46 | `filters`, `sorts`, `limit`, `query_timezone`) are the same as the `query` tool.
47 |
48 | Additionally, it accepts an optional `vis_config` parameter:
49 | - vis_config (optional): A JSON object that controls the default visualization
50 | settings for the generated query.
51 |
52 | vis_config Details:
53 | The `vis_config` object supports a wide range of properties for various chart types.
54 | Here are some notes on making visualizations.
55 |
56 | ### Cartesian Charts (Area, Bar, Column, Line, Scatter)
57 |
58 | These chart types share a large number of configuration options.
59 |
60 | **General**
61 | * `type`: The type of visualization (`looker_area`, `looker_bar`, `looker_column`, `looker_line`, `looker_scatter`).
62 | * `series_types`: Override the chart type for individual series.
63 | * `show_view_names`: Display view names in labels and tooltips (`true`/`false`).
64 | * `series_labels`: Provide custom names for series.
65 |
66 | **Styling & Colors**
67 | * `colors`: An array of color values to be used for the chart series.
68 | * `series_colors`: A mapping of series names to specific color values.
69 | * `color_application`: Advanced controls for color palette application (collection, palette, reverse, etc.).
70 | * `font_size`: Font size for labels (e.g., '12px').
71 |
72 | **Legend**
73 | * `hide_legend`: Show or hide the chart legend (`true`/`false`).
74 | * `legend_position`: Placement of the legend (`'center'`, `'left'`, `'right'`).
75 |
76 | **Axes**
77 | * `swap_axes`: Swap the X and Y axes (`true`/`false`).
78 | * `x_axis_scale`: Scale of the x-axis (`'auto'`, `'ordinal'`, `'linear'`, `'time'`).
79 | * `x_axis_reversed`, `y_axis_reversed`: Reverse the direction of an axis (`true`/`false`).
80 | * `x_axis_gridlines`, `y_axis_gridlines`: Display gridlines for an axis (`true`/`false`).
81 | * `show_x_axis_label`, `show_y_axis_label`: Show or hide the axis title (`true`/`false`).
82 | * `show_x_axis_ticks`, `show_y_axis_ticks`: Show or hide axis tick marks (`true`/`false`).
83 | * `x_axis_label`, `y_axis_label`: Set a custom title for an axis.
84 | * `x_axis_datetime_label`: A format string for datetime labels on the x-axis (e.g., `'%Y-%m'`).
85 | * `x_padding_left`, `x_padding_right`: Adjust padding on the ends of the x-axis.
86 | * `x_axis_label_rotation`, `x_axis_label_rotation_bar`: Set rotation for x-axis labels.
87 | * `x_axis_zoom`, `y_axis_zoom`: Enable zooming on an axis (`true`/`false`).
88 | * `y_axes`: An array of configuration objects for multiple y-axes.
89 |
90 | **Data & Series**
91 | * `stacking`: How to stack series (`''` for none, `'normal'`, `'percent'`).
92 | * `ordering`: Order of series in a stack (`'none'`, etc.).
93 | * `limit_displayed_rows`: Enable or disable limiting the number of rows displayed (`true`/`false`).
94 | * `limit_displayed_rows_values`: Configuration for the row limit (e.g., `{ "first_last": "first", "show_hide": "show", "num_rows": 10 }`).
95 | * `discontinuous_nulls`: How to render null values in line charts (`true`/`false`).
96 | * `point_style`: Style for points on line and area charts (`'none'`, `'circle'`, `'circle_outline'`).
97 | * `series_point_styles`: Override point styles for individual series.
98 | * `interpolation`: Line interpolation style (`'linear'`, `'monotone'`, `'step'`, etc.).
99 | * `show_value_labels`: Display values on data points (`true`/`false`).
100 | * `label_value_format`: A format string for value labels.
101 | * `show_totals_labels`: Display total labels on stacked charts (`true`/`false`).
102 | * `totals_color`: Color for total labels.
103 | * `show_silhouette`: Display a "silhouette" of hidden series in stacked charts (`true`/`false`).
104 | * `hidden_series`: An array of series names to hide from the visualization.
105 |
106 | **Scatter/Bubble Specific**
107 | * `size_by_field`: The field used to determine the size of bubbles.
108 | * `color_by_field`: The field used to determine the color of bubbles.
109 | * `plot_size_by_field`: Whether to display the size-by field in the legend.
110 | * `cluster_points`: Group nearby points into clusters (`true`/`false`).
111 | * `quadrants_enabled`: Display quadrants on the chart (`true`/`false`).
112 | * `quadrant_properties`: Configuration for quadrant labels and colors.
113 | * `custom_quadrant_value_x`, `custom_quadrant_value_y`: Set quadrant boundaries as a percentage.
114 | * `custom_quadrant_point_x`, `custom_quadrant_point_y`: Set quadrant boundaries to a specific value.
115 |
116 | **Miscellaneous**
117 | * `reference_lines`: Configuration for displaying reference lines.
118 | * `trend_lines`: Configuration for displaying trend lines.
119 | * `trellis`: Configuration for creating trellis (small multiple) charts.
120 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering interactions.
121 |
122 | ### Boxplot
123 |
124 | * Inherits most of the Cartesian chart options.
125 | * `type`: Must be `looker_boxplot`.
126 |
127 | ### Funnel
128 |
129 | * `type`: Must be `looker_funnel`.
130 | * `orientation`: How data is read (`'automatic'`, `'dataInRows'`, `'dataInColumns'`).
131 | * `percentType`: How percentages are calculated (`'percentOfMaxValue'`, `'percentOfPriorRow'`).
132 | * `labelPosition`, `valuePosition`, `percentPosition`: Placement of labels (`'left'`, `'right'`, `'inline'`, `'hidden'`).
133 | * `labelColor`, `labelColorEnabled`: Set a custom color for labels.
134 | * `labelOverlap`: Allow labels to overlap (`true`/`false`).
135 | * `barColors`: An array of colors for the funnel steps.
136 | * `color_application`: Advanced color palette controls.
137 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
138 |
139 | ### Pie / Donut
140 |
141 | * `type`: Must be `looker_pie`.
142 | * `value_labels`: Where to display values (`'legend'`, `'labels'`).
143 | * `label_type`: The format of data labels (`'labPer'`, `'labVal'`, `'lab'`, `'val'`, `'per'`).
144 | * `start_angle`, `end_angle`: The start and end angles of the pie chart.
145 | * `inner_radius`: The inner radius, used to create a donut chart.
146 | * `series_colors`, `series_labels`: Override colors and labels for specific slices.
147 | * `color_application`: Advanced color palette controls.
148 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
149 | * `advanced_vis_config`: A string containing JSON for advanced Highcharts configuration.
150 |
151 | ### Waterfall
152 |
153 | * Inherits most of the Cartesian chart options.
154 | * `type`: Must be `looker_waterfall`.
155 | * `up_color`: Color for positive (increasing) values.
156 | * `down_color`: Color for negative (decreasing) values.
157 | * `total_color`: Color for the total bar.
158 |
159 | ### Word Cloud
160 |
161 | * `type`: Must be `looker_wordcloud`.
162 | * `rotation`: Enable random word rotation (`true`/`false`).
163 | * `colors`: An array of colors for the words.
164 | * `color_application`: Advanced color palette controls.
165 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
166 |
167 | These are some sample vis_config settings.
168 |
169 | A bar chart -
170 | {{
171 | "defaults_version": 1,
172 | "label_density": 25,
173 | "legend_position": "center",
174 | "limit_displayed_rows": false,
175 | "ordering": "none",
176 | "plot_size_by_field": false,
177 | "point_style": "none",
178 | "show_null_labels": false,
179 | "show_silhouette": false,
180 | "show_totals_labels": false,
181 | "show_value_labels": false,
182 | "show_view_names": false,
183 | "show_x_axis_label": true,
184 | "show_x_axis_ticks": true,
185 | "show_y_axis_labels": true,
186 | "show_y_axis_ticks": true,
187 | "stacking": "normal",
188 | "totals_color": "#808080",
189 | "trellis": "",
190 | "type": "looker_bar",
191 | "x_axis_gridlines": false,
192 | "x_axis_reversed": false,
193 | "x_axis_scale": "auto",
194 | "x_axis_zoom": true,
195 | "y_axis_combined": true,
196 | "y_axis_gridlines": true,
197 | "y_axis_reversed": false,
198 | "y_axis_scale_mode": "linear",
199 | "y_axis_tick_density": "default",
200 | "y_axis_tick_density_custom": 5,
201 | "y_axis_zoom": true
202 | }}
203 |
204 | A column chart with an option advanced_vis_config -
205 | {{
206 | "advanced_vis_config": "{ chart: { type: 'pie', spacingBottom: 50, spacingLeft: 50, spacingRight: 50, spacingTop: 50, }, legend: { enabled: false, }, plotOptions: { pie: { dataLabels: { enabled: true, format: '\u003cb\u003e{key}\u003c/b\u003e\u003cspan style=\"font-weight: normal\"\u003e - {percentage:.2f}%\u003c/span\u003e', }, showInLegend: false, }, }, series: [], }",
207 | "colors": [
208 | "grey"
209 | ],
210 | "defaults_version": 1,
211 | "hidden_fields": [],
212 | "label_density": 25,
213 | "legend_position": "center",
214 | "limit_displayed_rows": false,
215 | "note_display": "below",
216 | "note_state": "collapsed",
217 | "note_text": "Unsold inventory only",
218 | "ordering": "none",
219 | "plot_size_by_field": false,
220 | "point_style": "none",
221 | "series_colors": {},
222 | "show_null_labels": false,
223 | "show_silhouette": false,
224 | "show_totals_labels": false,
225 | "show_value_labels": true,
226 | "show_view_names": false,
227 | "show_x_axis_label": true,
228 | "show_x_axis_ticks": true,
229 | "show_y_axis_labels": true,
230 | "show_y_axis_ticks": true,
231 | "stacking": "normal",
232 | "totals_color": "#808080",
233 | "trellis": "",
234 | "type": "looker_column",
235 | "x_axis_gridlines": false,
236 | "x_axis_reversed": false,
237 | "x_axis_scale": "auto",
238 | "x_axis_zoom": true,
239 | "y_axes": [],
240 | "y_axis_combined": true,
241 | "y_axis_gridlines": true,
242 | "y_axis_reversed": false,
243 | "y_axis_scale_mode": "linear",
244 | "y_axis_tick_density": "default",
245 | "y_axis_tick_density_custom": 5,
246 | "y_axis_zoom": true
247 | }}
248 |
249 | A line chart -
250 | {{
251 | "defaults_version": 1,
252 | "hidden_pivots": {},
253 | "hidden_series": [],
254 | "interpolation": "linear",
255 | "label_density": 25,
256 | "legend_position": "center",
257 | "limit_displayed_rows": false,
258 | "plot_size_by_field": false,
259 | "point_style": "none",
260 | "series_types": {},
261 | "show_null_points": true,
262 | "show_value_labels": false,
263 | "show_view_names": false,
264 | "show_x_axis_label": true,
265 | "show_x_axis_ticks": true,
266 | "show_y_axis_labels": true,
267 | "show_y_axis_ticks": true,
268 | "stacking": "",
269 | "trellis": "",
270 | "type": "looker_line",
271 | "x_axis_gridlines": false,
272 | "x_axis_reversed": false,
273 | "x_axis_scale": "auto",
274 | "y_axis_combined": true,
275 | "y_axis_gridlines": true,
276 | "y_axis_reversed": false,
277 | "y_axis_scale_mode": "linear",
278 | "y_axis_tick_density": "default",
279 | "y_axis_tick_density_custom": 5
280 | }}
281 |
282 | An area chart -
283 | {{
284 | "defaults_version": 1,
285 | "interpolation": "linear",
286 | "label_density": 25,
287 | "legend_position": "center",
288 | "limit_displayed_rows": false,
289 | "plot_size_by_field": false,
290 | "point_style": "none",
291 | "series_types": {},
292 | "show_null_points": true,
293 | "show_silhouette": false,
294 | "show_totals_labels": false,
295 | "show_value_labels": false,
296 | "show_view_names": false,
297 | "show_x_axis_label": true,
298 | "show_x_axis_ticks": true,
299 | "show_y_axis_labels": true,
300 | "show_y_axis_ticks": true,
301 | "stacking": "normal",
302 | "totals_color": "#808080",
303 | "trellis": "",
304 | "type": "looker_area",
305 | "x_axis_gridlines": false,
306 | "x_axis_reversed": false,
307 | "x_axis_scale": "auto",
308 | "x_axis_zoom": true,
309 | "y_axis_combined": true,
310 | "y_axis_gridlines": true,
311 | "y_axis_reversed": false,
312 | "y_axis_scale_mode": "linear",
313 | "y_axis_tick_density": "default",
314 | "y_axis_tick_density_custom": 5,
315 | "y_axis_zoom": true
316 | }}
317 |
318 | A scatter plot -
319 | {{
320 | "cluster_points": false,
321 | "custom_quadrant_point_x": 5,
322 | "custom_quadrant_point_y": 5,
323 | "custom_value_label_column": "",
324 | "custom_x_column": "",
325 | "custom_y_column": "",
326 | "defaults_version": 1,
327 | "hidden_fields": [],
328 | "hidden_pivots": {},
329 | "hidden_points_if_no": [],
330 | "hidden_series": [],
331 | "interpolation": "linear",
332 | "label_density": 25,
333 | "legend_position": "center",
334 | "limit_displayed_rows": false,
335 | "limit_displayed_rows_values": {
336 | "first_last": "first",
337 | "num_rows": 0,
338 | "show_hide": "hide"
339 | },
340 | "plot_size_by_field": false,
341 | "point_style": "circle",
342 | "quadrant_properties": {
343 | "0": {
344 | "color": "",
345 | "label": "Quadrant 1"
346 | },
347 | "1": {
348 | "color": "",
349 | "label": "Quadrant 2"
350 | },
351 | "2": {
352 | "color": "",
353 | "label": "Quadrant 3"
354 | },
355 | "3": {
356 | "color": "",
357 | "label": "Quadrant 4"
358 | }
359 | },
360 | "quadrants_enabled": false,
361 | "series_labels": {},
362 | "series_types": {},
363 | "show_null_points": false,
364 | "show_value_labels": false,
365 | "show_view_names": true,
366 | "show_x_axis_label": true,
367 | "show_x_axis_ticks": true,
368 | "show_y_axis_labels": true,
369 | "show_y_axis_ticks": true,
370 | "size_by_field": "roi",
371 | "stacking": "normal",
372 | "swap_axes": true,
373 | "trellis": "",
374 | "type": "looker_scatter",
375 | "x_axis_gridlines": false,
376 | "x_axis_reversed": false,
377 | "x_axis_scale": "auto",
378 | "x_axis_zoom": true,
379 | "y_axes": [
380 | {
381 | "label": "",
382 | "orientation": "bottom",
383 | "series": [
384 | {
385 | "axisId": "Channel_0 - average_of_roi_first",
386 | "id": "Channel_0 - average_of_roi_first",
387 | "name": "Channel_0"
388 | },
389 | {
390 | "axisId": "Channel_1 - average_of_roi_first",
391 | "id": "Channel_1 - average_of_roi_first",
392 | "name": "Channel_1"
393 | },
394 | {
395 | "axisId": "Channel_2 - average_of_roi_first",
396 | "id": "Channel_2 - average_of_roi_first",
397 | "name": "Channel_2"
398 | },
399 | {
400 | "axisId": "Channel_3 - average_of_roi_first",
401 | "id": "Channel_3 - average_of_roi_first",
402 | "name": "Channel_3"
403 | },
404 | {
405 | "axisId": "Channel_4 - average_of_roi_first",
406 | "id": "Channel_4 - average_of_roi_first",
407 | "name": "Channel_4"
408 | }
409 | ],
410 | "showLabels": true,
411 | "showValues": true,
412 | "tickDensity": "custom",
413 | "tickDensityCustom": 100,
414 | "type": "linear",
415 | "unpinAxis": false
416 | }
417 | ],
418 | "y_axis_combined": true,
419 | "y_axis_gridlines": true,
420 | "y_axis_reversed": false,
421 | "y_axis_scale_mode": "linear",
422 | "y_axis_tick_density": "default",
423 | "y_axis_tick_density_custom": 5,
424 | "y_axis_zoom": true
425 | }}
426 |
427 | A single record visualization -
428 | {{
429 | "defaults_version": 1,
430 | "show_view_names": false,
431 | "type": "looker_single_record"
432 | }}
433 |
434 | A single value visualization -
435 | {{
436 | "comparison_reverse_colors": false,
437 | "comparison_type": "value", "conditional_formatting_include_nulls": false, "conditional_formatting_include_totals": false,
438 | "custom_color": "#1A73E8",
439 | "custom_color_enabled": true,
440 | "defaults_version": 1,
441 | "enable_conditional_formatting": false,
442 | "series_types": {},
443 | "show_comparison": false,
444 | "show_comparison_label": true,
445 | "show_single_value_title": true,
446 | "single_value_title": "Total Clicks",
447 | "type": "single_value"
448 | }}
449 |
450 | A Pie chart -
451 | {{
452 | "defaults_version": 1,
453 | "label_density": 25,
454 | "label_type": "labPer",
455 | "legend_position": "center",
456 | "limit_displayed_rows": false,
457 | "ordering": "none",
458 | "plot_size_by_field": false,
459 | "point_style": "none",
460 | "series_types": {},
461 | "show_null_labels": false,
462 | "show_silhouette": false,
463 | "show_totals_labels": false,
464 | "show_value_labels": false,
465 | "show_view_names": false,
466 | "show_x_axis_label": true,
467 | "show_x_axis_ticks": true,
468 | "show_y_axis_labels": true,
469 | "show_y_axis_ticks": true,
470 | "stacking": "",
471 | "totals_color": "#808080",
472 | "trellis": "",
473 | "type": "looker_pie",
474 | "value_labels": "legend",
475 | "x_axis_gridlines": false,
476 | "x_axis_reversed": false,
477 | "x_axis_scale": "auto",
478 | "y_axis_combined": true,
479 | "y_axis_gridlines": true,
480 | "y_axis_reversed": false,
481 | "y_axis_scale_mode": "linear",
482 | "y_axis_tick_density": "default",
483 | "y_axis_tick_density_custom": 5
484 | }}
485 |
486 | The result is a JSON object with the id, slug, the url, and
487 | the long_url.
488 | ```
489 |
490 | ## Reference
491 |
492 | | **field** | **type** | **required** | **description** |
493 | |-------------|:--------:|:------------:|----------------------------------------------------|
494 | | kind | string | true | Must be "looker-query-url" |
495 | | source | string | true | Name of the source the SQL should execute on. |
496 | | description | string | true | Description of the tool that is passed to the LLM. |
497 |
```
--------------------------------------------------------------------------------
/internal/server/static/js/toolDisplay.js:
--------------------------------------------------------------------------------
```javascript
1 | // Copyright 2025 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { handleRunTool, displayResults } from './runTool.js';
16 | import { createGoogleAuthMethodItem } from './auth.js'
17 |
18 | /**
19 | * Helper function to create form inputs for parameters.
20 | */
21 | function createParamInput(param, toolId) {
22 | const paramItem = document.createElement('div');
23 | paramItem.className = 'param-item';
24 |
25 | const label = document.createElement('label');
26 | const INPUT_ID = `param-${toolId}-${param.name}`;
27 | const NAME_TEXT = document.createTextNode(param.name);
28 | label.setAttribute('for', INPUT_ID);
29 | label.appendChild(NAME_TEXT);
30 |
31 | const IS_AUTH_PARAM = param.authServices && param.authServices.length > 0;
32 | let additionalLabelText = '';
33 | if (IS_AUTH_PARAM) {
34 | additionalLabelText += ' (auth)';
35 | }
36 | if (!param.required) {
37 | additionalLabelText += ' (optional)';
38 | }
39 |
40 | if (additionalLabelText) {
41 | const additionalSpan = document.createElement('span');
42 | additionalSpan.textContent = additionalLabelText;
43 | additionalSpan.classList.add('param-label-extras');
44 | label.appendChild(additionalSpan);
45 | }
46 | paramItem.appendChild(label);
47 |
48 | const inputCheckboxWrapper = document.createElement('div');
49 | const inputContainer = document.createElement('div');
50 | inputCheckboxWrapper.className = 'input-checkbox-wrapper';
51 | inputContainer.className = 'param-input-element-container';
52 |
53 | // Build parameter's value input box.
54 | const PLACEHOLDER_LABEL = param.label;
55 | let inputElement;
56 | let boolValueLabel = null;
57 |
58 | if (param.type === 'textarea') {
59 | inputElement = document.createElement('textarea');
60 | inputElement.rows = 3;
61 | inputContainer.appendChild(inputElement);
62 | } else if(param.type === 'checkbox') {
63 | inputElement = document.createElement('input');
64 | inputElement.type = 'checkbox';
65 | inputElement.title = PLACEHOLDER_LABEL;
66 | inputElement.checked = false;
67 |
68 | // handle true/false label for boolean params
69 | boolValueLabel = document.createElement('span');
70 | boolValueLabel.className = 'checkbox-bool-label';
71 | boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
72 |
73 | inputContainer.appendChild(inputElement);
74 | inputContainer.appendChild(boolValueLabel);
75 |
76 | inputElement.addEventListener('change', () => {
77 | boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
78 | });
79 | } else {
80 | inputElement = document.createElement('input');
81 | inputElement.type = param.type;
82 | inputContainer.appendChild(inputElement);
83 | }
84 |
85 | inputElement.id = INPUT_ID;
86 | inputElement.name = param.name;
87 | inputElement.classList.add('param-input-element');
88 |
89 | if (IS_AUTH_PARAM) {
90 | inputElement.disabled = true;
91 | inputElement.classList.add('auth-param-input');
92 | if (param.type !== 'checkbox') {
93 | inputElement.placeholder = param.authServices;
94 | }
95 | } else if (param.type !== 'checkbox') {
96 | inputElement.placeholder = PLACEHOLDER_LABEL ? PLACEHOLDER_LABEL.trim() : '';
97 | }
98 | inputCheckboxWrapper.appendChild(inputContainer);
99 |
100 | // create the "Include Param" checkbox
101 | const INCLUDE_CHECKBOX_ID = `include-${INPUT_ID}`;
102 | const includeContainer = document.createElement('div');
103 | const includeCheckbox = document.createElement('input');
104 |
105 | includeContainer.className = 'include-param-container';
106 | includeCheckbox.type = 'checkbox';
107 | includeCheckbox.id = INCLUDE_CHECKBOX_ID;
108 | includeCheckbox.name = `include-${param.name}`;
109 | includeCheckbox.title = 'Include this parameter'; // Add a tooltip
110 |
111 | // default to checked, unless it's an optional parameter
112 | includeCheckbox.checked = param.required;
113 |
114 | includeContainer.appendChild(includeCheckbox);
115 | inputCheckboxWrapper.appendChild(includeContainer);
116 |
117 | paramItem.appendChild(inputCheckboxWrapper);
118 |
119 | // function to update UI based on checkbox state
120 | const updateParamIncludedState = () => {
121 | const isIncluded = includeCheckbox.checked;
122 | if (isIncluded) {
123 | paramItem.classList.remove('disabled-param');
124 | if (!IS_AUTH_PARAM) {
125 | inputElement.disabled = false;
126 | }
127 | if (boolValueLabel) {
128 | boolValueLabel.classList.remove('disabled');
129 | }
130 | } else {
131 | paramItem.classList.add('disabled-param');
132 | inputElement.disabled = true;
133 | if (boolValueLabel) {
134 | boolValueLabel.classList.add('disabled');
135 | }
136 | }
137 | };
138 |
139 | // add event listener to the include checkbox
140 | includeCheckbox.addEventListener('change', updateParamIncludedState);
141 | updateParamIncludedState();
142 |
143 | return paramItem;
144 | }
145 |
146 | /**
147 | * Function to create the header editor popup modal.
148 | * @param {string} toolId The unique identifier for the tool.
149 | * @param {!Object<string, string>} currentHeaders The current headers.
150 | * @param {function(!Object<string, string>): void} saveCallback A function to be
151 | * called when the "Save" button is clicked and the headers are successfully
152 | * parsed. The function receives the updated headers object as its argument.
153 | * @return {!HTMLDivElement} The outermost div element of the created modal.
154 | */
155 | function createHeaderEditorModal(toolId, currentHeaders, toolParameters, authRequired, saveCallback) {
156 | const MODAL_ID = `header-modal-${toolId}`;
157 | let modal = document.getElementById(MODAL_ID);
158 |
159 | if (modal) {
160 | modal.remove();
161 | }
162 |
163 | modal = document.createElement('div');
164 | modal.id = MODAL_ID;
165 | modal.className = 'header-modal';
166 |
167 | const modalContent = document.createElement('div');
168 | const modalHeader = document.createElement('h5');
169 | const headersTextarea = document.createElement('textarea');
170 |
171 | modalContent.className = 'header-modal-content';
172 | modalHeader.textContent = 'Edit Request Headers';
173 | headersTextarea.id = `headers-textarea-${toolId}`;
174 | headersTextarea.className = 'headers-textarea';
175 | headersTextarea.rows = 10;
176 | headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
177 |
178 | // handle authenticated params
179 | const authProfileNames = new Set();
180 | toolParameters.forEach(param => {
181 | const isAuthParam = param.authServices && param.authServices.length > 0;
182 | if (isAuthParam && param.authServices) {
183 | param.authServices.forEach(name => authProfileNames.add(name));
184 | }
185 | });
186 |
187 | // handle authorized invocations
188 | if (authRequired && authRequired.length > 0) {
189 | authRequired.forEach(name => authProfileNames.add(name));
190 | }
191 |
192 | modalContent.appendChild(modalHeader);
193 | modalContent.appendChild(headersTextarea);
194 |
195 | if (authProfileNames.size > 0 || authRequired.length > 0) {
196 | const authHelperSection = document.createElement('div');
197 | authHelperSection.className = 'auth-helper-section';
198 | const authList = document.createElement('div');
199 | authList.className = 'auth-method-list';
200 |
201 | authProfileNames.forEach(profileName => {
202 | const authItem = createGoogleAuthMethodItem(toolId, profileName);
203 | authList.appendChild(authItem);
204 | });
205 | authHelperSection.appendChild(authList);
206 | modalContent.appendChild(authHelperSection);
207 | }
208 |
209 | const modalActions = document.createElement('div');
210 | const closeButton = document.createElement('button');
211 | const saveButton = document.createElement('button');
212 | const authTokenDropdown = createAuthTokenInfoDropdown();
213 |
214 | modalActions.className = 'header-modal-actions';
215 | closeButton.textContent = 'Close';
216 | closeButton.className = 'btn btn--closeHeaders';
217 | closeButton.addEventListener('click', () => closeHeaderEditor(toolId));
218 | saveButton.textContent = 'Save';
219 | saveButton.className = 'btn btn--saveHeaders';
220 | saveButton.addEventListener('click', () => {
221 | try {
222 | const updatedHeaders = JSON.parse(headersTextarea.value);
223 | saveCallback(updatedHeaders);
224 | closeHeaderEditor(toolId);
225 | } catch (e) {
226 | alert('Invalid JSON format for headers.');
227 | console.error("Header JSON parse error:", e);
228 | }
229 | });
230 |
231 | modalActions.appendChild(closeButton);
232 | modalActions.appendChild(saveButton);
233 | modalContent.appendChild(modalActions);
234 | modalContent.appendChild(authTokenDropdown);
235 | modal.appendChild(modalContent);
236 |
237 | return modal;
238 | }
239 |
240 | /**
241 | * Function to open the header popup.
242 | */
243 | function openHeaderEditor(toolId) {
244 | const modal = document.getElementById(`header-modal-${toolId}`);
245 | if (modal) {
246 | modal.style.display = 'block';
247 | }
248 | }
249 |
250 | /**
251 | * Function to close the header popup.
252 | */
253 | function closeHeaderEditor(toolId) {
254 | const modal = document.getElementById(`header-modal-${toolId}`);
255 | if (modal) {
256 | modal.style.display = 'none';
257 | }
258 | }
259 |
260 | /**
261 | * Creates a dropdown element showing information on how to extract Google auth tokens.
262 | * @return {HTMLDetailsElement} The details element representing the dropdown.
263 | */
264 | function createAuthTokenInfoDropdown() {
265 | const details = document.createElement('details');
266 | const summary = document.createElement('summary');
267 | const content = document.createElement('div');
268 |
269 | details.className = 'auth-token-details';
270 | details.appendChild(summary);
271 | summary.textContent = 'How to extract Google OAuth ID Token manually';
272 | content.className = 'auth-token-content';
273 |
274 | // auth instruction dropdown
275 | const tabButtons = document.createElement('div');
276 | const leftTab = document.createElement('button');
277 | const rightTab = document.createElement('button');
278 |
279 | tabButtons.className = 'auth-tab-group';
280 | leftTab.className = 'auth-tab-picker active';
281 | leftTab.textContent = 'With Standard Account';
282 | leftTab.setAttribute('data-tab', 'standard');
283 | rightTab.className = 'auth-tab-picker';
284 | rightTab.textContent = 'With Service Account';
285 | rightTab.setAttribute('data-tab', 'service');
286 |
287 | tabButtons.appendChild(leftTab);
288 | tabButtons.appendChild(rightTab);
289 | content.appendChild(tabButtons);
290 |
291 | const tabContentContainer = document.createElement('div');
292 | const standardAccInstructions = document.createElement('div');
293 | const serviceAccInstructions = document.createElement('div');
294 |
295 | standardAccInstructions.id = 'auth-tab-standard';
296 | standardAccInstructions.className = 'auth-tab-content active';
297 | standardAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_STANDARD;
298 | serviceAccInstructions.id = 'auth-tab-service';
299 | serviceAccInstructions.className = 'auth-tab-content';
300 | serviceAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT;
301 |
302 | tabContentContainer.appendChild(standardAccInstructions);
303 | tabContentContainer.appendChild(serviceAccInstructions);
304 | content.appendChild(tabContentContainer);
305 |
306 | // switching tabs logic
307 | const tabBtns = [leftTab, rightTab];
308 | const tabContents = [standardAccInstructions, serviceAccInstructions];
309 |
310 | tabBtns.forEach(btn => {
311 | btn.addEventListener('click', () => {
312 | // deactivate all buttons and contents
313 | tabBtns.forEach(b => b.classList.remove('active'));
314 | tabContents.forEach(c => c.classList.remove('active'));
315 |
316 | btn.classList.add('active');
317 |
318 | const tabId = btn.getAttribute('data-tab');
319 | const activeContent = content.querySelector(`#auth-tab-${tabId}`);
320 | if (activeContent) {
321 | activeContent.classList.add('active');
322 | }
323 | });
324 | });
325 |
326 | details.appendChild(content);
327 | return details;
328 | }
329 |
330 | /**
331 | * Renders the tool display area.
332 | */
333 | export function renderToolInterface(tool, containerElement) {
334 | const TOOL_ID = tool.id;
335 | containerElement.innerHTML = '';
336 |
337 | let lastResults = null;
338 | let currentHeaders = {
339 | "Content-Type": "application/json"
340 | };
341 |
342 | // function to update lastResults so we can toggle json
343 | const updateLastResults = (newResults) => {
344 | lastResults = newResults;
345 | };
346 | const updateCurrentHeaders = (newHeaders) => {
347 | currentHeaders = newHeaders;
348 | const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
349 | containerElement.appendChild(newModal);
350 | };
351 |
352 | const gridContainer = document.createElement('div');
353 | gridContainer.className = 'tool-details-grid';
354 |
355 | const toolInfoContainer = document.createElement('div');
356 | const nameBox = document.createElement('div');
357 | const descBox = document.createElement('div');
358 |
359 | nameBox.className = 'tool-box tool-name';
360 | nameBox.innerHTML = `<h5>Name:</h5><p>${tool.name}</p>`;
361 | descBox.className = 'tool-box tool-description';
362 | descBox.innerHTML = `<h5>Description:</h5><p>${tool.description}</p>`;
363 |
364 | toolInfoContainer.className = 'tool-info';
365 | toolInfoContainer.appendChild(nameBox);
366 | toolInfoContainer.appendChild(descBox);
367 | gridContainer.appendChild(toolInfoContainer);
368 |
369 | const DISLCAIMER_INFO = "*Checked parameters are sent with the value from their text field. Empty fields will be sent as an empty string. To exclude a parameter, uncheck it."
370 | const paramsContainer = document.createElement('div');
371 | const form = document.createElement('form');
372 | const paramsHeader = document.createElement('div');
373 | const disclaimerText = document.createElement('div');
374 |
375 | paramsContainer.className = 'tool-params tool-box';
376 | paramsContainer.innerHTML = '<h5>Parameters:</h5>';
377 | paramsHeader.className = 'params-header';
378 | paramsContainer.appendChild(paramsHeader);
379 | disclaimerText.textContent = DISLCAIMER_INFO;
380 | disclaimerText.className = 'params-disclaimer';
381 | paramsContainer.appendChild(disclaimerText);
382 |
383 | form.id = `tool-params-form-${TOOL_ID}`;
384 |
385 | tool.parameters.forEach(param => {
386 | form.appendChild(createParamInput(param, TOOL_ID));
387 | });
388 | paramsContainer.appendChild(form);
389 | gridContainer.appendChild(paramsContainer);
390 |
391 | containerElement.appendChild(gridContainer);
392 |
393 | const RESPONSE_AREA_ID = `tool-response-area-${TOOL_ID}`;
394 | const runButtonContainer = document.createElement('div');
395 | const editHeadersButton = document.createElement('button');
396 | const runButton = document.createElement('button');
397 |
398 | editHeadersButton.className = 'btn btn--editHeaders';
399 | editHeadersButton.textContent = 'Edit Headers';
400 | editHeadersButton.addEventListener('click', () => openHeaderEditor(TOOL_ID));
401 | runButtonContainer.className = 'run-button-container';
402 | runButtonContainer.appendChild(editHeadersButton);
403 |
404 | runButton.className = 'btn btn--run';
405 | runButton.textContent = 'Run Tool';
406 | runButtonContainer.appendChild(runButton);
407 | containerElement.appendChild(runButtonContainer);
408 |
409 | // response Area (bottom)
410 | const responseContainer = document.createElement('div');
411 | const responseHeaderControls = document.createElement('div');
412 | const responseHeader = document.createElement('h5');
413 | const responseArea = document.createElement('textarea');
414 |
415 | responseContainer.className = 'tool-response tool-box';
416 | responseHeaderControls.className = 'response-header-controls';
417 | responseHeader.textContent = 'Response:';
418 | responseHeaderControls.appendChild(responseHeader);
419 |
420 | // prettify box
421 | const PRETTIFY_ID = `prettify-${TOOL_ID}`;
422 | const prettifyDiv = document.createElement('div');
423 | const prettifyLabel = document.createElement('label');
424 | const prettifyCheckbox = document.createElement('input');
425 |
426 | prettifyDiv.className = 'prettify-container';
427 | prettifyLabel.setAttribute('for', PRETTIFY_ID);
428 | prettifyLabel.textContent = 'Prettify JSON';
429 | prettifyLabel.className = 'prettify-label';
430 |
431 | prettifyCheckbox.type = 'checkbox';
432 | prettifyCheckbox.id = PRETTIFY_ID;
433 | prettifyCheckbox.checked = true;
434 | prettifyCheckbox.className = 'prettify-checkbox';
435 |
436 | prettifyDiv.appendChild(prettifyLabel);
437 | prettifyDiv.appendChild(prettifyCheckbox);
438 |
439 | responseHeaderControls.appendChild(prettifyDiv);
440 | responseContainer.appendChild(responseHeaderControls);
441 |
442 | responseArea.id = RESPONSE_AREA_ID;
443 | responseArea.readOnly = true;
444 | responseArea.placeholder = 'Results will appear here...';
445 | responseArea.className = 'tool-response-area';
446 | responseArea.rows = 10;
447 | responseContainer.appendChild(responseArea);
448 |
449 | containerElement.appendChild(responseContainer);
450 |
451 | // create and append the header editor modal
452 | const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
453 | containerElement.appendChild(headerModal);
454 |
455 | prettifyCheckbox.addEventListener('change', () => {
456 | if (lastResults) {
457 | displayResults(lastResults, responseArea, prettifyCheckbox.checked);
458 | }
459 | });
460 |
461 | runButton.addEventListener('click', (event) => {
462 | event.preventDefault();
463 | handleRunTool(TOOL_ID, form, responseArea, tool.parameters, prettifyCheckbox, updateLastResults, currentHeaders);
464 | });
465 | }
466 |
467 | /**
468 | * Checks if a specific parameter is marked as included for a given tool.
469 | * @param {string} toolId The ID of the tool.
470 | * @param {string} paramName The name of the parameter.
471 | * @return {boolean|null} True if the parameter's include checkbox is checked,
472 | * False if unchecked, Null if the checkbox element is not found.
473 | */
474 | export function isParamIncluded(toolId, paramName) {
475 | const inputId = `param-${toolId}-${paramName}`;
476 | const includeCheckboxId = `include-${inputId}`;
477 | const includeCheckbox = document.getElementById(includeCheckboxId);
478 |
479 | if (includeCheckbox && includeCheckbox.type === 'checkbox') {
480 | return includeCheckbox.checked;
481 | }
482 |
483 | console.warn(`Include checkbox not found for ID: ${includeCheckboxId}`);
484 | return null;
485 | }
486 |
487 | // Templates for inserting token retrieval instructions into edit header modal
488 | const AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT = `
489 | <p>To obtain a Google OAuth ID token using a service account:</p>
490 | <ol>
491 | <li>Make sure you are on the intended SERVICE account (typically contain iam.gserviceaccount.com). Verify by running the command below.
492 | <pre><code>gcloud auth list</code></pre>
493 | </li>
494 | <li>Print an id token with the audience set to your clientID defined in tools file:
495 | <pre><code>gcloud auth print-identity-token --audiences=YOUR_CLIENT_ID_HERE</code></pre>
496 | </li>
497 | <li>Copy the output token.</li>
498 | <li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
499 | <pre><code>{
500 | "Content-Type": "application/json",
501 | "my-google-auth_token": "YOUR_ID_TOKEN_HERE"
502 | } </code></pre>
503 | </li>
504 | </ol>
505 | <p>This token is typically short-lived.</p>`;
506 |
507 | const AUTH_TOKEN_INSTRUCTIONS_STANDARD = `
508 | <p>To obtain a Google OAuth ID token using a standard account:</p>
509 | <ol>
510 | <li>Make sure you are on your intended standard account. Verify by running the command below.
511 | <pre><code>gcloud auth list</code></pre>
512 | </li>
513 | <li>Within your Cloud Console, add the following link to the "Authorized Redirect URIs".</li>
514 | <pre><code>https://developers.google.com/oauthplayground</code></pre>
515 | <li>Go to the Google OAuth Playground site: <a href="https://developers.google.com/oauthplayground/" target="_blank">https://developers.google.com/oauthplayground/</a></li>
516 | <li>In the top right settings menu, select "Use your own OAuth Credentials".</li>
517 | <li>Input your clientID (from tools file), along with the client secret from Cloud Console.</li>
518 | <li>Inside the Google OAuth Playground, select "Google OAuth2 API v2.</li>
519 | <ul>
520 | <li>Select "Authorize APIs".</li>
521 | <li>Select "Exchange Authorization codes for tokens"</li>
522 | <li>Copy the id_token field provided in the response.</li>
523 | </ul>
524 | <li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
525 | <pre><code>{
526 | "Content-Type": "application/json",
527 | "my-google-auth_token": "YOUR_ID_TOKEN_HERE"
528 | } </code></pre>
529 | </li>
530 | </ol>
531 | <p>This token is typically short-lived.</p>`;
```
--------------------------------------------------------------------------------
/tests/mindsdb/mindsdb_integration_test.go:
--------------------------------------------------------------------------------
```go
1 | // Copyright 2025 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package mindsdb
16 |
17 | import (
18 | "bytes"
19 | "context"
20 | "database/sql"
21 | "fmt"
22 | "net/http"
23 | "os"
24 | "regexp"
25 | "strings"
26 | "testing"
27 | "time"
28 |
29 | _ "github.com/go-sql-driver/mysql"
30 | "github.com/google/uuid"
31 | "github.com/googleapis/genai-toolbox/internal/testutils"
32 | "github.com/googleapis/genai-toolbox/tests"
33 | )
34 |
35 | var (
36 | MindsDBSourceKind = "mindsdb"
37 | MindsDBToolKind = "mindsdb-sql"
38 | MindsDBDatabase = os.Getenv("MINDSDB_DATABASE")
39 | MindsDBHost = os.Getenv("MINDSDB_HOST")
40 | MindsDBPort = os.Getenv("MINDSDB_PORT")
41 | MindsDBUser = os.Getenv("MINDSDB_USER")
42 | MindsDBPass = os.Getenv("MINDSDB_PASS")
43 | )
44 |
45 | func getMindsDBVars(t *testing.T) map[string]any {
46 | switch "" {
47 | case MindsDBDatabase:
48 | t.Fatal("'MINDSDB_DATABASE' not set")
49 | case MindsDBHost:
50 | t.Fatal("'MINDSDB_HOST' not set")
51 | case MindsDBPort:
52 | t.Fatal("'MINDSDB_PORT' not set")
53 | case MindsDBUser:
54 | t.Fatal("'MINDSDB_USER' not set")
55 | }
56 |
57 | // MindsDBPass can be empty, but the env var must exist
58 | if _, exists := os.LookupEnv("MINDSDB_PASS"); !exists {
59 | t.Fatal("'MINDSDB_PASS' not set (can be empty)")
60 | }
61 |
62 | // Handle no-password authentication
63 | mindsdbPassword := MindsDBPass
64 | if mindsdbPassword == "none" {
65 | mindsdbPassword = ""
66 | }
67 |
68 | return map[string]any{
69 | "kind": MindsDBSourceKind,
70 | "host": MindsDBHost,
71 | "port": MindsDBPort,
72 | "database": MindsDBDatabase,
73 | "user": MindsDBUser,
74 | "password": mindsdbPassword,
75 | }
76 | }
77 |
78 | // initMindsDBConnectionPool creates a connection pool using MySQL protocol
79 | func initMindsDBConnectionPool(host, port, user, pass, dbname string) (*sql.DB, error) {
80 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname)
81 | pool, err := sql.Open("mysql", dsn)
82 | if err != nil {
83 | return nil, fmt.Errorf("sql.Open: %w", err)
84 | }
85 | return pool, nil
86 | }
87 |
88 | func TestMindsDBToolEndpoints(t *testing.T) {
89 | sourceConfig := getMindsDBVars(t)
90 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
91 | defer cancel()
92 |
93 | var args []string
94 |
95 | // Create unique table names with UUID
96 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
97 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
98 | // Tool statements with ORDER BY for consistent results
99 | paramToolStmt := fmt.Sprintf("SELECT * FROM files.%s WHERE id = ? OR name = ? ORDER BY id", tableNameParam)
100 | idParamToolStmt := fmt.Sprintf("SELECT * FROM files.%s WHERE id = ? ORDER BY id", tableNameParam)
101 | nameParamToolStmt := fmt.Sprintf("SELECT * FROM files.%s WHERE name = ? ORDER BY id", tableNameParam)
102 | authToolStmt := fmt.Sprintf("SELECT name FROM files.%s WHERE email = ? ORDER BY name", tableNameAuth)
103 |
104 | toolsFile := map[string]any{
105 | "sources": map[string]any{
106 | "my-instance": sourceConfig,
107 | },
108 | "authServices": map[string]any{
109 | "my-google-auth": map[string]any{
110 | "kind": "google",
111 | "clientId": tests.ClientId,
112 | },
113 | },
114 | "tools": map[string]any{
115 | "my-simple-tool": map[string]any{
116 | "kind": MindsDBToolKind,
117 | "source": "my-instance",
118 | "description": "Simple tool to test end to end functionality.",
119 | "statement": "SELECT 1",
120 | },
121 | "my-tool": map[string]any{
122 | "kind": MindsDBToolKind,
123 | "source": "my-instance",
124 | "description": "Tool to test invocation with params.",
125 | "statement": paramToolStmt,
126 | "parameters": []map[string]any{
127 | {
128 | "name": "id",
129 | "type": "integer",
130 | "description": "user ID",
131 | },
132 | {
133 | "name": "name",
134 | "type": "string",
135 | "description": "user name",
136 | },
137 | },
138 | },
139 | "my-tool-by-id": map[string]any{
140 | "kind": MindsDBToolKind,
141 | "source": "my-instance",
142 | "description": "Tool to test invocation with params.",
143 | "statement": idParamToolStmt,
144 | "parameters": []map[string]any{
145 | {
146 | "name": "id",
147 | "type": "integer",
148 | "description": "user ID",
149 | },
150 | },
151 | },
152 | "my-tool-by-name": map[string]any{
153 | "kind": MindsDBToolKind,
154 | "source": "my-instance",
155 | "description": "Tool to test invocation with params.",
156 | "statement": nameParamToolStmt,
157 | "parameters": []map[string]any{
158 | {
159 | "name": "name",
160 | "type": "string",
161 | "description": "user name",
162 | "required": false,
163 | },
164 | },
165 | },
166 | "my-array-tool": map[string]any{
167 | "kind": MindsDBToolKind,
168 | "source": "my-instance",
169 | "description": "Tool to test invocation with array params.",
170 | "statement": "SELECT 1 as id, 'Alice' as name UNION SELECT 3 as id, 'Sid' as name",
171 | },
172 | "my-auth-tool": map[string]any{
173 | "kind": MindsDBToolKind,
174 | "source": "my-instance",
175 | "description": "Tool to test authenticated parameters.",
176 | "statement": authToolStmt,
177 | "parameters": []map[string]any{
178 | {
179 | "name": "email",
180 | "type": "string",
181 | "description": "user email",
182 | "authServices": []map[string]string{
183 | {
184 | "name": "my-google-auth",
185 | "field": "email",
186 | },
187 | },
188 | },
189 | },
190 | },
191 | "my-auth-required-tool": map[string]any{
192 | "kind": MindsDBToolKind,
193 | "source": "my-instance",
194 | "description": "Tool to test auth required invocation.",
195 | "statement": "SELECT 1",
196 | "authRequired": []string{
197 | "my-google-auth",
198 | },
199 | },
200 | "my-fail-tool": map[string]any{
201 | "kind": MindsDBToolKind,
202 | "source": "my-instance",
203 | "description": "Tool to test statement with incorrect syntax.",
204 | "statement": "INVALID SQL STATEMENT",
205 | },
206 | "my-exec-sql-tool": map[string]any{
207 | "kind": "mindsdb-execute-sql",
208 | "source": "my-instance",
209 | "description": "Tool to execute sql",
210 | },
211 | "my-auth-exec-sql-tool": map[string]any{
212 | "kind": "mindsdb-execute-sql",
213 | "source": "my-instance",
214 | "description": "Tool to execute sql with auth",
215 | "authRequired": []string{
216 | "my-google-auth",
217 | },
218 | },
219 | },
220 | }
221 |
222 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
223 | if err != nil {
224 | t.Fatalf("command initialization returned an error: %s", err)
225 | }
226 | defer cleanup()
227 |
228 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
229 | defer cancel()
230 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
231 | if err != nil {
232 | t.Logf("toolbox command logs: \n%s", out)
233 | t.Fatalf("toolbox didn't start successfully: %s", err)
234 | }
235 |
236 | // Create connection pool and test tables with sample data
237 | pool, err := initMindsDBConnectionPool(MindsDBHost, MindsDBPort, MindsDBUser, MindsDBPass, MindsDBDatabase)
238 | if err != nil {
239 | t.Fatalf("unable to create MindsDB connection pool: %s", err)
240 | }
241 | defer pool.Close()
242 |
243 | // Create param table: id=1:Alice, id=2:Jane, id=3:Sid, id=4:null
244 | createParamSQL := fmt.Sprintf("CREATE TABLE files.%s (SELECT 1 as id, 'Alice' as name UNION ALL SELECT 2, 'Jane' UNION ALL SELECT 3, 'Sid' UNION ALL SELECT 4, NULL)", tableNameParam)
245 | _, err = pool.ExecContext(ctx, createParamSQL)
246 | if err != nil {
247 | t.Fatalf("unable to create param table: %s", err)
248 | }
249 |
250 | // Create auth table: id=1:Alice:test@..., id=2:Jane:jane@...
251 | createAuthSQL := fmt.Sprintf("CREATE TABLE files.%s (SELECT 1 as id, 'Alice' as name, '%s' as email UNION ALL SELECT 2, 'Jane', '[email protected]')", tableNameAuth, tests.ServiceAccountEmail)
252 | _, err = pool.ExecContext(ctx, createAuthSQL)
253 | if err != nil {
254 | t.Fatalf("unable to create auth table: %s", err)
255 | }
256 |
257 | defer func() {
258 | _, _ = pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS files.%s", tableNameParam))
259 | _, _ = pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS files.%s", tableNameAuth))
260 | }()
261 |
262 | select1Want := "[{\"1\":1}]"
263 |
264 | // Run standard tool tests with MindsDB-specific expectations
265 | tests.RunToolGetTest(t)
266 | tests.RunToolInvokeTest(t, select1Want,
267 | tests.DisableArrayTest(), // MindsDB doesn't support array parameters
268 | )
269 |
270 | t.Run("mindsdb_core_functionality", func(t *testing.T) {
271 | tests.RunToolInvokeSimpleTest(t, "my-simple-tool", select1Want)
272 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1"}`), select1Want)
273 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1+1 as result"}`), "[{\"result\":2}]")
274 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 'hello' as greeting"}`), "[{\"greeting\":\"hello\"}]")
275 | })
276 |
277 | t.Run("mindsdb_sql_tests", func(t *testing.T) {
278 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1"}`), select1Want)
279 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW DATABASES"}`), "")
280 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SHOW TABLES"}`), "")
281 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT TABLE_NAME FROM information_schema.TABLES LIMIT 1"}`), "")
282 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT 1+1 as result"}`), "")
283 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT UPPER('hello') as result"}`), "")
284 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool", []byte(`{"sql": "SELECT NOW() as current_time"}`), "")
285 | })
286 |
287 | // Test CREATE DATABASE (MindsDB's federated database capability)
288 | t.Run("mindsdb_create_database", func(t *testing.T) {
289 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
290 | []byte(`{"sql": "DROP DATABASE IF EXISTS test_postgres_db"}`), "")
291 |
292 | // Create external database integration using MindsDB's demo database
293 | createDBSQL := `CREATE DATABASE test_postgres_db WITH ENGINE = 'postgres', PARAMETERS = {'user': 'demo_user', 'password': 'demo_password', 'host': 'samples.mindsdb.com', 'port': '5432', 'database': 'demo', 'schema': 'demo_data'}`
294 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
295 | []byte(`{"sql": "`+createDBSQL+`"}`), "")
296 |
297 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
298 | []byte(`{"sql": "SHOW DATABASES"}`), "")
299 |
300 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
301 | []byte(`{"sql": "SHOW TABLES FROM test_postgres_db"}`), "")
302 |
303 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
304 | []byte(`{"sql": "DROP DATABASE IF EXISTS test_postgres_db"}`), "")
305 | })
306 |
307 | // Test MindsDB integration capabilities with product/review data
308 | t.Run("mindsdb_integration_demo", func(t *testing.T) {
309 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
310 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_products"}`), "")
311 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
312 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_reviews"}`), "")
313 |
314 | // Create test tables with sample data
315 | createProductsSQL := `CREATE TABLE files.test_products (SELECT 'PROD001' as product_id, 'Laptop Computer' as product_name, 'Electronics' as category UNION ALL SELECT 'PROD002', 'Office Chair', 'Furniture' UNION ALL SELECT 'PROD003', 'Coffee Maker', 'Appliances' UNION ALL SELECT 'PROD004', 'Desk Lamp', 'Furniture')`
316 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
317 | []byte(`{"sql": "`+createProductsSQL+`"}`), "")
318 |
319 | createReviewsSQL := `CREATE TABLE files.test_reviews (SELECT 'PROD001' as product_id, 'Great laptop, very fast!' as review, 5 as rating UNION ALL SELECT 'PROD001', 'Good value for money', 4 UNION ALL SELECT 'PROD002', 'Very comfortable chair', 5 UNION ALL SELECT 'PROD002', 'Nice design but expensive', 3 UNION ALL SELECT 'PROD003', 'Makes excellent coffee', 5 UNION ALL SELECT 'PROD004', 'Bright light, perfect for reading', 4)`
320 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
321 | []byte(`{"sql": "`+createReviewsSQL+`"}`), "")
322 |
323 | t.Run("query_created_tables", func(t *testing.T) {
324 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
325 | []byte(`{"sql": "SELECT * FROM files.test_products ORDER BY product_id"}`), "")
326 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
327 | []byte(`{"sql": "SELECT * FROM files.test_reviews ORDER BY product_id, rating DESC"}`), "")
328 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
329 | []byte(`{"sql": "SELECT category, COUNT(*) as product_count FROM files.test_products GROUP BY category ORDER BY category"}`), "")
330 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
331 | []byte(`{"sql": "SELECT product_id, AVG(rating) as avg_rating FROM files.test_reviews GROUP BY product_id ORDER BY avg_rating DESC"}`), "")
332 | })
333 |
334 | t.Run("cross_database_join", func(t *testing.T) {
335 | joinSQL := `SELECT p.product_name, p.category, r.review, r.rating FROM files.test_products p JOIN files.test_reviews r ON p.product_id = r.product_id WHERE r.rating >= 4 ORDER BY p.product_name, r.rating DESC`
336 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
337 | []byte(`{"sql": "`+joinSQL+`"}`), "")
338 |
339 | aggSQL := `SELECT p.category, COUNT(DISTINCT p.product_id) as product_count, COUNT(r.review) as review_count, AVG(r.rating) as avg_rating FROM files.test_products p LEFT JOIN files.test_reviews r ON p.product_id = r.product_id GROUP BY p.category ORDER BY avg_rating DESC`
340 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
341 | []byte(`{"sql": "`+aggSQL+`"}`), "")
342 | })
343 |
344 | t.Run("advanced_sql_features", func(t *testing.T) {
345 | subquerySQL := `SELECT p.product_name, p.category, AVG(r.rating) as avg_rating FROM files.test_products p JOIN files.test_reviews r ON p.product_id = r.product_id GROUP BY p.product_id, p.product_name, p.category HAVING AVG(r.rating) >= 4 ORDER BY avg_rating DESC`
346 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
347 | []byte(`{"sql": "`+subquerySQL+`"}`), "")
348 |
349 | caseSQL := `SELECT product_id, review, rating, CASE WHEN rating >= 5 THEN 'Excellent' WHEN rating >= 4 THEN 'Good' WHEN rating >= 3 THEN 'Average' ELSE 'Poor' END as rating_category FROM files.test_reviews ORDER BY rating DESC, product_id`
350 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
351 | []byte(`{"sql": "`+caseSQL+`"}`), "")
352 | })
353 |
354 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
355 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_product_summary"}`), "")
356 |
357 | t.Run("data_manipulation", func(t *testing.T) {
358 | summarySQL := `CREATE TABLE files.test_product_summary (SELECT p.product_id, p.product_name, p.category, COUNT(r.review) as total_reviews, AVG(r.rating) as avg_rating, MAX(r.rating) as max_rating, MIN(r.rating) as min_rating FROM files.test_products p LEFT JOIN files.test_reviews r ON p.product_id = r.product_id GROUP BY p.product_id, p.product_name, p.category)`
359 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
360 | []byte(`{"sql": "`+summarySQL+`"}`), "")
361 |
362 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
363 | []byte(`{"sql": "SELECT * FROM files.test_product_summary ORDER BY avg_rating DESC"}`), "")
364 | })
365 |
366 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
367 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_products"}`), "")
368 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
369 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_reviews"}`), "")
370 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
371 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_product_summary"}`), "")
372 | })
373 |
374 | // Test database integration and cross-database joins
375 | t.Run("mindsdb_create_database_integration", func(t *testing.T) {
376 | showDBSQL := `SHOW DATABASES`
377 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
378 | []byte(`{"sql": "`+showDBSQL+`"}`), "")
379 |
380 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
381 | []byte(`{"sql": "SHOW TABLES FROM files"}`), "")
382 |
383 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
384 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_integration_data"}`), "")
385 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
386 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_local_data"}`), "")
387 |
388 | createIntegrationTableSQL := `CREATE TABLE files.test_integration_data (SELECT 1 as id, 'Data from integration' as description, CURDATE() as created_at UNION ALL SELECT 2, 'Another record', CURDATE() UNION ALL SELECT 3, 'Third record', CURDATE())`
389 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
390 | []byte(`{"sql": "`+createIntegrationTableSQL+`"}`), "")
391 |
392 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
393 | []byte(`{"sql": "SELECT * FROM files.test_integration_data ORDER BY id"}`), "")
394 |
395 | createLocalTableSQL := `CREATE TABLE files.test_local_data (SELECT 1 as id, 'Local metadata' as metadata UNION ALL SELECT 2, 'More metadata')`
396 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
397 | []byte(`{"sql": "`+createLocalTableSQL+`"}`), "")
398 |
399 | crossJoinSQL := `SELECT i.id, i.description, l.metadata FROM files.test_integration_data i LEFT JOIN files.test_local_data l ON i.id = l.id ORDER BY i.id`
400 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
401 | []byte(`{"sql": "`+crossJoinSQL+`"}`), "")
402 |
403 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
404 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_integration_data"}`), "")
405 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
406 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_local_data"}`), "")
407 | })
408 |
409 | // Test data transformation with customer order data
410 | t.Run("mindsdb_data_transformation", func(t *testing.T) {
411 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
412 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_orders"}`), "")
413 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
414 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_customer_summary"}`), "")
415 |
416 | createOrdersSQL := `CREATE TABLE files.test_orders (SELECT 1 as order_id, 'CUST001' as customer_id, 100.50 as amount, '2024-01-15' as order_date UNION ALL SELECT 2, 'CUST001', 250.00, '2024-02-20' UNION ALL SELECT 3, 'CUST002', 75.25, '2024-01-18' UNION ALL SELECT 4, 'CUST003', 500.00, '2024-03-10' UNION ALL SELECT 5, 'CUST002', 150.00, '2024-02-25')`
417 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
418 | []byte(`{"sql": "`+createOrdersSQL+`"}`), "")
419 |
420 | customerSummarySQL := `CREATE TABLE files.test_customer_summary (SELECT customer_id, COUNT(*) as total_orders, SUM(amount) as total_spent, AVG(amount) as avg_order_value, MIN(order_date) as first_order_date, MAX(order_date) as last_order_date FROM files.test_orders GROUP BY customer_id)`
421 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
422 | []byte(`{"sql": "`+customerSummarySQL+`"}`), "")
423 |
424 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
425 | []byte(`{"sql": "SELECT * FROM files.test_customer_summary ORDER BY total_spent DESC"}`), "")
426 |
427 | segmentSQL := `SELECT customer_id, total_spent, CASE WHEN total_spent >= 300 THEN 'High Value' WHEN total_spent >= 150 THEN 'Medium Value' ELSE 'Low Value' END as customer_segment FROM files.test_customer_summary ORDER BY total_spent DESC`
428 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
429 | []byte(`{"sql": "`+segmentSQL+`"}`), "")
430 |
431 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
432 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_orders"}`), "")
433 | tests.RunToolInvokeParametersTest(t, "my-exec-sql-tool",
434 | []byte(`{"sql": "DROP TABLE IF EXISTS files.test_customer_summary"}`), "")
435 | })
436 |
437 | // Test error handling - these are expected to fail but exercise error paths
438 | t.Run("mindsdb_error_handling", func(t *testing.T) {
439 | // Test invalid SQL - expect this to fail with 400
440 | resp, err := http.Post("http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke", "application/json", bytes.NewBuffer([]byte(`{"sql": "INVALID SQL QUERY"}`)))
441 | if err != nil {
442 | t.Fatalf("error when sending request: %s", err)
443 | }
444 | defer resp.Body.Close()
445 | if resp.StatusCode != http.StatusBadRequest {
446 | t.Logf("Expected 400 for invalid SQL, got %d (this exercises error handling)", resp.StatusCode)
447 | }
448 |
449 | // Test empty SQL - expect this to fail with 400
450 | resp2, err := http.Post("http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke", "application/json", bytes.NewBuffer([]byte(`{"sql": ""}`)))
451 | if err != nil {
452 | t.Fatalf("error when sending request: %s", err)
453 | }
454 | defer resp2.Body.Close()
455 | if resp2.StatusCode != http.StatusBadRequest {
456 | t.Logf("Expected 400 for empty SQL, got %d (this exercises error handling)", resp2.StatusCode)
457 | }
458 | })
459 |
460 | // Test authentication - these are expected to fail but exercise auth code paths
461 | t.Run("mindsdb_auth_tests", func(t *testing.T) {
462 | // Test auth-required tool without auth - expect this to fail with 401
463 | resp, err := http.Post("http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke", "application/json", bytes.NewBuffer([]byte(`{"sql": "SELECT 1"}`)))
464 | if err != nil {
465 | t.Fatalf("error when sending request: %s", err)
466 | }
467 | defer resp.Body.Close()
468 | if resp.StatusCode != http.StatusUnauthorized {
469 | t.Logf("Expected 401 for missing auth, got %d (this exercises auth handling)", resp.StatusCode)
470 | }
471 | })
472 | }
473 |
```
--------------------------------------------------------------------------------
/internal/tools/neo4j/neo4jschema/neo4jschema.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 neo4jschema
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "sync"
21 | "time"
22 |
23 | "github.com/goccy/go-yaml"
24 | "github.com/googleapis/genai-toolbox/internal/embeddingmodels"
25 | "github.com/googleapis/genai-toolbox/internal/sources"
26 | "github.com/googleapis/genai-toolbox/internal/tools"
27 | "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jschema/cache"
28 | "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jschema/helpers"
29 | "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jschema/types"
30 | "github.com/googleapis/genai-toolbox/internal/util/parameters"
31 | "github.com/neo4j/neo4j-go-driver/v5/neo4j"
32 | )
33 |
34 | // kind defines the unique identifier for this tool.
35 | const kind string = "neo4j-schema"
36 |
37 | // init registers the tool with the application's tool registry when the package is initialized.
38 | func init() {
39 | if !tools.Register(kind, newConfig) {
40 | panic(fmt.Sprintf("tool kind %q already registered", kind))
41 | }
42 | }
43 |
44 | // newConfig decodes a YAML configuration into a Config struct.
45 | // This function is called by the tool registry to create a new configuration object.
46 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
47 | actual := Config{Name: name}
48 | if err := decoder.DecodeContext(ctx, &actual); err != nil {
49 | return nil, err
50 | }
51 | return actual, nil
52 | }
53 |
54 | // compatibleSource defines the interface a data source must implement to be used by this tool.
55 | // It ensures that the source can provide a Neo4j driver and database name.
56 | type compatibleSource interface {
57 | Neo4jDriver() neo4j.DriverWithContext
58 | Neo4jDatabase() string
59 | }
60 |
61 | // Config holds the configuration settings for the Neo4j schema tool.
62 | // These settings are typically read from a YAML file.
63 | type Config struct {
64 | Name string `yaml:"name" validate:"required"`
65 | Kind string `yaml:"kind" validate:"required"`
66 | Source string `yaml:"source" validate:"required"`
67 | Description string `yaml:"description" validate:"required"`
68 | AuthRequired []string `yaml:"authRequired"`
69 | CacheExpireMinutes *int `yaml:"cacheExpireMinutes,omitempty"` // Cache expiration time in minutes.
70 | }
71 |
72 | // Statically verify that Config implements the tools.ToolConfig interface.
73 | var _ tools.ToolConfig = Config{}
74 |
75 | // ToolConfigKind returns the kind of this tool configuration.
76 | func (cfg Config) ToolConfigKind() string {
77 | return kind
78 | }
79 |
80 | // Initialize sets up the tool with its dependencies and returns a ready-to-use Tool instance.
81 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
82 |
83 | params := parameters.Parameters{}
84 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, nil)
85 |
86 | // Set a default cache expiration if not provided in the configuration.
87 | if cfg.CacheExpireMinutes == nil {
88 | defaultExpiration := cache.DefaultExpiration // Default to 60 minutes
89 | cfg.CacheExpireMinutes = &defaultExpiration
90 | }
91 |
92 | // Finish tool setup by creating the Tool instance.
93 | t := Tool{
94 | Config: cfg,
95 | cache: cache.NewCache(),
96 | manifest: tools.Manifest{Description: cfg.Description, Parameters: params.Manifest(), AuthRequired: cfg.AuthRequired},
97 | mcpManifest: mcpManifest,
98 | }
99 | return t, nil
100 | }
101 |
102 | // Statically verify that Tool implements the tools.Tool interface.
103 | var _ tools.Tool = Tool{}
104 |
105 | // Tool represents the Neo4j schema extraction tool.
106 | // It holds the Neo4j driver, database information, and a cache for the schema.
107 | type Tool struct {
108 | Config
109 | cache *cache.Cache
110 | manifest tools.Manifest
111 | mcpManifest tools.McpManifest
112 | }
113 |
114 | // Invoke executes the tool's main logic: fetching the Neo4j schema.
115 | // It first checks the cache for a valid schema before extracting it from the database.
116 | func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
117 | source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Kind)
118 | if err != nil {
119 | return nil, err
120 | }
121 |
122 | // Check if a valid schema is already in the cache.
123 | if cachedSchema, ok := t.cache.Get("schema"); ok {
124 | if schema, ok := cachedSchema.(*types.SchemaInfo); ok {
125 | return schema, nil
126 | }
127 | }
128 |
129 | // If not cached, extract the schema from the database.
130 | schema, err := t.extractSchema(ctx, source)
131 | if err != nil {
132 | return nil, fmt.Errorf("failed to extract database schema: %w", err)
133 | }
134 |
135 | // Cache the newly extracted schema for future use.
136 | expiration := time.Duration(*t.CacheExpireMinutes) * time.Minute
137 | t.cache.Set("schema", schema, expiration)
138 |
139 | return schema, nil
140 | }
141 |
142 | // ParseParams is a placeholder as this tool does not require input parameters.
143 | func (t Tool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (parameters.ParamValues, error) {
144 | return parameters.ParamValues{}, nil
145 | }
146 |
147 | func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
148 | return parameters.ParamValues{}, nil
149 | }
150 |
151 | // Manifest returns the tool's manifest, which describes its purpose and parameters.
152 | func (t Tool) Manifest() tools.Manifest {
153 | return t.manifest
154 | }
155 |
156 | // McpManifest returns the machine-consumable manifest for the tool.
157 | func (t Tool) McpManifest() tools.McpManifest {
158 | return t.mcpManifest
159 | }
160 |
161 | // Authorized checks if the tool is authorized to run based on the provided authentication services.
162 | func (t Tool) Authorized(verifiedAuthServices []string) bool {
163 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
164 | }
165 |
166 | func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
167 | return false, nil
168 | }
169 |
170 | // checkAPOCProcedures verifies if essential APOC procedures are available in the database.
171 | // It returns true only if all required procedures are found.
172 | func (t Tool) checkAPOCProcedures(ctx context.Context, source compatibleSource) (bool, error) {
173 | proceduresToCheck := []string{"apoc.meta.schema", "apoc.meta.cypher.types"}
174 |
175 | session := source.Neo4jDriver().NewSession(ctx, neo4j.SessionConfig{DatabaseName: source.Neo4jDatabase()})
176 | defer session.Close(ctx)
177 |
178 | // This query efficiently counts how many of the specified procedures exist.
179 | query := "SHOW PROCEDURES YIELD name WHERE name IN $procs RETURN count(name) AS procCount"
180 | params := map[string]any{"procs": proceduresToCheck}
181 |
182 | result, err := session.Run(ctx, query, params)
183 | if err != nil {
184 | return false, fmt.Errorf("failed to execute procedure check query: %w", err)
185 | }
186 |
187 | record, err := result.Single(ctx)
188 | if err != nil {
189 | return false, fmt.Errorf("failed to retrieve single result for procedure check: %w", err)
190 | }
191 |
192 | rawCount, found := record.Get("procCount")
193 | if !found {
194 | return false, fmt.Errorf("field 'procCount' not found in result record")
195 | }
196 |
197 | procCount, ok := rawCount.(int64)
198 | if !ok {
199 | return false, fmt.Errorf("expected 'procCount' to be of type int64, but got %T", rawCount)
200 | }
201 |
202 | // Return true only if the number of found procedures matches the number we were looking for.
203 | return procCount == int64(len(proceduresToCheck)), nil
204 | }
205 |
206 | // extractSchema orchestrates the concurrent extraction of different parts of the database schema.
207 | // It runs several extraction tasks in parallel for efficiency.
208 | func (t Tool) extractSchema(ctx context.Context, source compatibleSource) (*types.SchemaInfo, error) {
209 | schema := &types.SchemaInfo{}
210 | var mu sync.Mutex
211 |
212 | // Define the different schema extraction tasks.
213 | tasks := []struct {
214 | name string
215 | fn func() error
216 | }{
217 | {
218 | name: "database-info",
219 | fn: func() error {
220 | dbInfo, err := t.extractDatabaseInfo(ctx, source)
221 | if err != nil {
222 | return fmt.Errorf("failed to extract database info: %w", err)
223 | }
224 | mu.Lock()
225 | defer mu.Unlock()
226 | schema.DatabaseInfo = *dbInfo
227 | return nil
228 | },
229 | },
230 | {
231 | name: "schema-extraction",
232 | fn: func() error {
233 | // Check if APOC procedures are available.
234 | hasAPOC, err := t.checkAPOCProcedures(ctx, source)
235 | if err != nil {
236 | return fmt.Errorf("failed to check APOC procedures: %w", err)
237 | }
238 |
239 | var nodeLabels []types.NodeLabel
240 | var relationships []types.Relationship
241 | var stats *types.Statistics
242 |
243 | // Use APOC if available for a more detailed schema; otherwise, use native queries.
244 | if hasAPOC {
245 | nodeLabels, relationships, stats, err = t.GetAPOCSchema(ctx, source)
246 | } else {
247 | nodeLabels, relationships, stats, err = t.GetSchemaWithoutAPOC(ctx, source, 100)
248 | }
249 | if err != nil {
250 | return fmt.Errorf("failed to get schema: %w", err)
251 | }
252 |
253 | mu.Lock()
254 | defer mu.Unlock()
255 | schema.NodeLabels = nodeLabels
256 | schema.Relationships = relationships
257 | schema.Statistics = *stats
258 | return nil
259 | },
260 | },
261 | {
262 | name: "constraints",
263 | fn: func() error {
264 | constraints, err := t.extractConstraints(ctx, source)
265 | if err != nil {
266 | return fmt.Errorf("failed to extract constraints: %w", err)
267 | }
268 | mu.Lock()
269 | defer mu.Unlock()
270 | schema.Constraints = constraints
271 | return nil
272 | },
273 | },
274 | {
275 | name: "indexes",
276 | fn: func() error {
277 | indexes, err := t.extractIndexes(ctx, source)
278 | if err != nil {
279 | return fmt.Errorf("failed to extract indexes: %w", err)
280 | }
281 | mu.Lock()
282 | defer mu.Unlock()
283 | schema.Indexes = indexes
284 | return nil
285 | },
286 | },
287 | }
288 |
289 | var wg sync.WaitGroup
290 | errCh := make(chan error, len(tasks))
291 |
292 | // Execute all tasks concurrently.
293 | for _, task := range tasks {
294 | wg.Add(1)
295 | go func(task struct {
296 | name string
297 | fn func() error
298 | }) {
299 | defer wg.Done()
300 | if err := task.fn(); err != nil {
301 | errCh <- err
302 | }
303 | }(task)
304 | }
305 |
306 | wg.Wait()
307 | close(errCh)
308 |
309 | // Collect any errors that occurred during the concurrent tasks.
310 | for err := range errCh {
311 | if err != nil {
312 | schema.Errors = append(schema.Errors, err.Error())
313 | }
314 | }
315 | return schema, nil
316 | }
317 |
318 | // GetAPOCSchema extracts schema information using the APOC library, which provides detailed metadata.
319 | func (t Tool) GetAPOCSchema(ctx context.Context, source compatibleSource) ([]types.NodeLabel, []types.Relationship, *types.Statistics, error) {
320 | var nodeLabels []types.NodeLabel
321 | var relationships []types.Relationship
322 | stats := &types.Statistics{
323 | NodesByLabel: make(map[string]int64),
324 | RelationshipsByType: make(map[string]int64),
325 | PropertiesByLabel: make(map[string]int64),
326 | PropertiesByRelType: make(map[string]int64),
327 | }
328 |
329 | var mu sync.Mutex
330 | var firstErr error
331 | ctx, cancel := context.WithCancel(ctx)
332 | defer cancel()
333 |
334 | handleError := func(err error) {
335 | mu.Lock()
336 | defer mu.Unlock()
337 | if firstErr == nil {
338 | firstErr = err
339 | cancel() // Cancel other operations on the first error.
340 | }
341 | }
342 |
343 | tasks := []struct {
344 | name string
345 | fn func(session neo4j.SessionWithContext) error
346 | }{
347 | {
348 | name: "apoc-schema",
349 | fn: func(session neo4j.SessionWithContext) error {
350 | result, err := session.Run(ctx, "CALL apoc.meta.schema({sample: 10}) YIELD value RETURN value", nil)
351 | if err != nil {
352 | return fmt.Errorf("failed to run APOC schema query: %w", err)
353 | }
354 | if !result.Next(ctx) {
355 | return fmt.Errorf("no results from APOC schema query")
356 | }
357 | schemaMap, ok := result.Record().Values[0].(map[string]any)
358 | if !ok {
359 | return fmt.Errorf("unexpected result format from APOC schema query: %T", result.Record().Values[0])
360 | }
361 | apocSchema, err := helpers.MapToAPOCSchema(schemaMap)
362 | if err != nil {
363 | return fmt.Errorf("failed to convert schema map to APOCSchemaResult: %w", err)
364 | }
365 | nodes, _, apocStats := helpers.ProcessAPOCSchema(apocSchema)
366 | mu.Lock()
367 | defer mu.Unlock()
368 | nodeLabels = nodes
369 | stats.TotalNodes = apocStats.TotalNodes
370 | stats.TotalProperties += apocStats.TotalProperties
371 | stats.NodesByLabel = apocStats.NodesByLabel
372 | stats.PropertiesByLabel = apocStats.PropertiesByLabel
373 | return nil
374 | },
375 | },
376 | {
377 | name: "apoc-relationships",
378 | fn: func(session neo4j.SessionWithContext) error {
379 | query := `
380 | MATCH (startNode)-[rel]->(endNode)
381 | WITH
382 | labels(startNode)[0] AS startNode,
383 | type(rel) AS relType,
384 | apoc.meta.cypher.types(rel) AS relProperties,
385 | labels(endNode)[0] AS endNode,
386 | count(*) AS count
387 | RETURN relType, startNode, endNode, relProperties, count`
388 | result, err := session.Run(ctx, query, nil)
389 | if err != nil {
390 | return fmt.Errorf("failed to extract relationships: %w", err)
391 | }
392 | for result.Next(ctx) {
393 | record := result.Record()
394 | relType, startNode, endNode := record.Values[0].(string), record.Values[1].(string), record.Values[2].(string)
395 | properties, count := record.Values[3].(map[string]any), record.Values[4].(int64)
396 |
397 | if relType == "" || count == 0 {
398 | continue
399 | }
400 | relationship := types.Relationship{Type: relType, StartNode: startNode, EndNode: endNode, Count: count, Properties: []types.PropertyInfo{}}
401 | for prop, propType := range properties {
402 | relationship.Properties = append(relationship.Properties, types.PropertyInfo{Name: prop, Types: []string{propType.(string)}})
403 | }
404 | mu.Lock()
405 | relationships = append(relationships, relationship)
406 | stats.RelationshipsByType[relType] += count
407 | stats.TotalRelationships += count
408 | propCount := int64(len(relationship.Properties))
409 | stats.TotalProperties += propCount
410 | stats.PropertiesByRelType[relType] += propCount
411 | mu.Unlock()
412 | }
413 | mu.Lock()
414 | defer mu.Unlock()
415 | if len(stats.RelationshipsByType) == 0 {
416 | stats.RelationshipsByType = nil
417 | }
418 | if len(stats.PropertiesByRelType) == 0 {
419 | stats.PropertiesByRelType = nil
420 | }
421 | return nil
422 | },
423 | },
424 | }
425 |
426 | var wg sync.WaitGroup
427 | wg.Add(len(tasks))
428 | for _, task := range tasks {
429 | go func(task struct {
430 | name string
431 | fn func(session neo4j.SessionWithContext) error
432 | }) {
433 | defer wg.Done()
434 | session := source.Neo4jDriver().NewSession(ctx, neo4j.SessionConfig{DatabaseName: source.Neo4jDatabase()})
435 | defer session.Close(ctx)
436 | if err := task.fn(session); err != nil {
437 | handleError(fmt.Errorf("task %s failed: %w", task.name, err))
438 | }
439 | }(task)
440 | }
441 | wg.Wait()
442 |
443 | if firstErr != nil {
444 | return nil, nil, nil, firstErr
445 | }
446 | return nodeLabels, relationships, stats, nil
447 | }
448 |
449 | // GetSchemaWithoutAPOC extracts schema information using native Cypher queries.
450 | // This serves as a fallback for databases without APOC installed.
451 | func (t Tool) GetSchemaWithoutAPOC(ctx context.Context, source compatibleSource, sampleSize int) ([]types.NodeLabel, []types.Relationship, *types.Statistics, error) {
452 | nodePropsMap := make(map[string]map[string]map[string]bool)
453 | relPropsMap := make(map[string]map[string]map[string]bool)
454 | nodeCounts := make(map[string]int64)
455 | relCounts := make(map[string]int64)
456 | relConnectivity := make(map[string]types.RelConnectivityInfo)
457 |
458 | var mu sync.Mutex
459 | var firstErr error
460 | ctx, cancel := context.WithCancel(ctx)
461 | defer cancel()
462 |
463 | handleError := func(err error) {
464 | mu.Lock()
465 | defer mu.Unlock()
466 | if firstErr == nil {
467 | firstErr = err
468 | cancel()
469 | }
470 | }
471 |
472 | tasks := []struct {
473 | name string
474 | fn func(session neo4j.SessionWithContext) error
475 | }{
476 | {
477 | name: "node-schema",
478 | fn: func(session neo4j.SessionWithContext) error {
479 | countResult, err := session.Run(ctx, `MATCH (n) UNWIND labels(n) AS label RETURN label, count(*) AS count ORDER BY count DESC`, nil)
480 | if err != nil {
481 | return fmt.Errorf("node count query failed: %w", err)
482 | }
483 | var labelsList []string
484 | mu.Lock()
485 | for countResult.Next(ctx) {
486 | record := countResult.Record()
487 | label, count := record.Values[0].(string), record.Values[1].(int64)
488 | nodeCounts[label] = count
489 | labelsList = append(labelsList, label)
490 | }
491 | mu.Unlock()
492 | if err = countResult.Err(); err != nil {
493 | return fmt.Errorf("node count result error: %w", err)
494 | }
495 |
496 | for _, label := range labelsList {
497 | propQuery := fmt.Sprintf(`MATCH (n:%s) WITH n LIMIT $sampleSize UNWIND keys(n) AS key WITH key, n[key] AS value WHERE value IS NOT NULL RETURN key, COLLECT(DISTINCT valueType(value)) AS types`, label)
498 | propResult, err := session.Run(ctx, propQuery, map[string]any{"sampleSize": sampleSize})
499 | if err != nil {
500 | return fmt.Errorf("node properties query for label %s failed: %w", label, err)
501 | }
502 | mu.Lock()
503 | if nodePropsMap[label] == nil {
504 | nodePropsMap[label] = make(map[string]map[string]bool)
505 | }
506 | for propResult.Next(ctx) {
507 | record := propResult.Record()
508 | key, types := record.Values[0].(string), record.Values[1].([]any)
509 | if nodePropsMap[label][key] == nil {
510 | nodePropsMap[label][key] = make(map[string]bool)
511 | }
512 | for _, tp := range types {
513 | nodePropsMap[label][key][tp.(string)] = true
514 | }
515 | }
516 | mu.Unlock()
517 | if err = propResult.Err(); err != nil {
518 | return fmt.Errorf("node properties result error for label %s: %w", label, err)
519 | }
520 | }
521 | return nil
522 | },
523 | },
524 | {
525 | name: "relationship-schema",
526 | fn: func(session neo4j.SessionWithContext) error {
527 | relQuery := `
528 | MATCH (start)-[r]->(end)
529 | WITH type(r) AS relType, labels(start) AS startLabels, labels(end) AS endLabels, count(*) AS count
530 | RETURN relType, CASE WHEN size(startLabels) > 0 THEN startLabels[0] ELSE null END AS startLabel, CASE WHEN size(endLabels) > 0 THEN endLabels[0] ELSE null END AS endLabel, sum(count) AS totalCount
531 | ORDER BY totalCount DESC`
532 | relResult, err := session.Run(ctx, relQuery, nil)
533 | if err != nil {
534 | return fmt.Errorf("relationship count query failed: %w", err)
535 | }
536 | var relTypesList []string
537 | mu.Lock()
538 | for relResult.Next(ctx) {
539 | record := relResult.Record()
540 | relType := record.Values[0].(string)
541 | startLabel := ""
542 | if record.Values[1] != nil {
543 | startLabel = record.Values[1].(string)
544 | }
545 | endLabel := ""
546 | if record.Values[2] != nil {
547 | endLabel = record.Values[2].(string)
548 | }
549 | count := record.Values[3].(int64)
550 | relCounts[relType] = count
551 | relTypesList = append(relTypesList, relType)
552 | if existing, ok := relConnectivity[relType]; !ok || count > existing.Count {
553 | relConnectivity[relType] = types.RelConnectivityInfo{StartNode: startLabel, EndNode: endLabel, Count: count}
554 | }
555 | }
556 | mu.Unlock()
557 | if err = relResult.Err(); err != nil {
558 | return fmt.Errorf("relationship count result error: %w", err)
559 | }
560 |
561 | for _, relType := range relTypesList {
562 | propQuery := fmt.Sprintf(`MATCH ()-[r:%s]->() WITH r LIMIT $sampleSize WHERE size(keys(r)) > 0 UNWIND keys(r) AS key WITH key, r[key] AS value WHERE value IS NOT NULL RETURN key, COLLECT(DISTINCT valueType(value)) AS types`, relType)
563 | propResult, err := session.Run(ctx, propQuery, map[string]any{"sampleSize": sampleSize})
564 | if err != nil {
565 | return fmt.Errorf("relationship properties query for type %s failed: %w", relType, err)
566 | }
567 | mu.Lock()
568 | if relPropsMap[relType] == nil {
569 | relPropsMap[relType] = make(map[string]map[string]bool)
570 | }
571 | for propResult.Next(ctx) {
572 | record := propResult.Record()
573 | key, propTypes := record.Values[0].(string), record.Values[1].([]any)
574 | if relPropsMap[relType][key] == nil {
575 | relPropsMap[relType][key] = make(map[string]bool)
576 | }
577 | for _, t := range propTypes {
578 | relPropsMap[relType][key][t.(string)] = true
579 | }
580 | }
581 | mu.Unlock()
582 | if err = propResult.Err(); err != nil {
583 | return fmt.Errorf("relationship properties result error for type %s: %w", relType, err)
584 | }
585 | }
586 | return nil
587 | },
588 | },
589 | }
590 |
591 | var wg sync.WaitGroup
592 | wg.Add(len(tasks))
593 | for _, task := range tasks {
594 | go func(task struct {
595 | name string
596 | fn func(session neo4j.SessionWithContext) error
597 | }) {
598 | defer wg.Done()
599 | session := source.Neo4jDriver().NewSession(ctx, neo4j.SessionConfig{DatabaseName: source.Neo4jDatabase()})
600 | defer session.Close(ctx)
601 | if err := task.fn(session); err != nil {
602 | handleError(fmt.Errorf("task %s failed: %w", task.name, err))
603 | }
604 | }(task)
605 | }
606 | wg.Wait()
607 |
608 | if firstErr != nil {
609 | return nil, nil, nil, firstErr
610 | }
611 |
612 | nodeLabels, relationships, stats := helpers.ProcessNonAPOCSchema(nodeCounts, nodePropsMap, relCounts, relPropsMap, relConnectivity)
613 | return nodeLabels, relationships, stats, nil
614 | }
615 |
616 | // extractDatabaseInfo retrieves general information about the Neo4j database instance.
617 | func (t Tool) extractDatabaseInfo(ctx context.Context, source compatibleSource) (*types.DatabaseInfo, error) {
618 | session := source.Neo4jDriver().NewSession(ctx, neo4j.SessionConfig{DatabaseName: source.Neo4jDatabase()})
619 | defer session.Close(ctx)
620 |
621 | result, err := session.Run(ctx, "CALL dbms.components() YIELD name, versions, edition", nil)
622 | if err != nil {
623 | return nil, err
624 | }
625 |
626 | dbInfo := &types.DatabaseInfo{}
627 | if result.Next(ctx) {
628 | record := result.Record()
629 | dbInfo.Name = record.Values[0].(string)
630 | if versions, ok := record.Values[1].([]any); ok && len(versions) > 0 {
631 | dbInfo.Version = versions[0].(string)
632 | }
633 | dbInfo.Edition = record.Values[2].(string)
634 | }
635 | return dbInfo, result.Err()
636 | }
637 |
638 | // extractConstraints fetches all schema constraints from the database.
639 | func (t Tool) extractConstraints(ctx context.Context, source compatibleSource) ([]types.Constraint, error) {
640 | session := source.Neo4jDriver().NewSession(ctx, neo4j.SessionConfig{DatabaseName: source.Neo4jDatabase()})
641 | defer session.Close(ctx)
642 |
643 | result, err := session.Run(ctx, "SHOW CONSTRAINTS", nil)
644 | if err != nil {
645 | return nil, err
646 | }
647 |
648 | var constraints []types.Constraint
649 | for result.Next(ctx) {
650 | record := result.Record().AsMap()
651 | constraint := types.Constraint{
652 | Name: helpers.GetStringValue(record["name"]),
653 | Type: helpers.GetStringValue(record["type"]),
654 | EntityType: helpers.GetStringValue(record["entityType"]),
655 | }
656 | if labels, ok := record["labelsOrTypes"].([]any); ok && len(labels) > 0 {
657 | constraint.Label = labels[0].(string)
658 | }
659 | if props, ok := record["properties"].([]any); ok {
660 | constraint.Properties = helpers.ConvertToStringSlice(props)
661 | }
662 | constraints = append(constraints, constraint)
663 | }
664 | return constraints, result.Err()
665 | }
666 |
667 | // extractIndexes fetches all schema indexes from the database.
668 | func (t Tool) extractIndexes(ctx context.Context, source compatibleSource) ([]types.Index, error) {
669 | session := source.Neo4jDriver().NewSession(ctx, neo4j.SessionConfig{DatabaseName: source.Neo4jDatabase()})
670 | defer session.Close(ctx)
671 |
672 | result, err := session.Run(ctx, "SHOW INDEXES", nil)
673 | if err != nil {
674 | return nil, err
675 | }
676 |
677 | var indexes []types.Index
678 | for result.Next(ctx) {
679 | record := result.Record().AsMap()
680 | index := types.Index{
681 | Name: helpers.GetStringValue(record["name"]),
682 | State: helpers.GetStringValue(record["state"]),
683 | Type: helpers.GetStringValue(record["type"]),
684 | EntityType: helpers.GetStringValue(record["entityType"]),
685 | }
686 | if labels, ok := record["labelsOrTypes"].([]any); ok && len(labels) > 0 {
687 | index.Label = labels[0].(string)
688 | }
689 | if props, ok := record["properties"].([]any); ok {
690 | index.Properties = helpers.ConvertToStringSlice(props)
691 | }
692 | indexes = append(indexes, index)
693 | }
694 | return indexes, result.Err()
695 | }
696 |
697 | func (t Tool) ToConfig() tools.ToolConfig {
698 | return t.Config
699 | }
700 |
701 | func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
702 | return "Authorization", nil
703 | }
704 |
```