This is page 15 of 47. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-create-project-file.md │ │ │ ├── looker-delete-project-file.md │ │ │ ├── looker-dev-mode.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-get-project-file.md │ │ │ ├── looker-get-project-files.md │ │ │ ├── looker-get-projects.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ ├── looker-run-look.md │ │ │ └── looker-update-project-file.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookercreateprojectfile │ │ │ │ ├── lookercreateprojectfile_test.go │ │ │ │ └── lookercreateprojectfile.go │ │ │ ├── lookerdeleteprojectfile │ │ │ │ ├── lookerdeleteprojectfile_test.go │ │ │ │ └── lookerdeleteprojectfile.go │ │ │ ├── lookerdevmode │ │ │ │ ├── lookerdevmode_test.go │ │ │ │ └── lookerdevmode.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookergetprojectfile │ │ │ │ ├── lookergetprojectfile_test.go │ │ │ │ └── lookergetprojectfile.go │ │ │ ├── lookergetprojectfiles │ │ │ │ ├── lookergetprojectfiles_test.go │ │ │ │ └── lookergetprojectfiles.go │ │ │ ├── lookergetprojects │ │ │ │ ├── lookergetprojects_test.go │ │ │ │ └── lookergetprojects.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ ├── lookerrunlook │ │ │ │ ├── lookerrunlook_test.go │ │ │ │ └── lookerrunlook.go │ │ │ └── lookerupdateprojectfile │ │ │ ├── lookerupdateprojectfile_test.go │ │ │ └── lookerupdateprojectfile.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /internal/tools/mssql/mssqlsql/mssqlsql_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 mssqlsql_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqlsql" 26 | ) 27 | 28 | func TestParseFromYamlMssql(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: mssql-sql 44 | source: my-instance 45 | description: some description 46 | statement: | 47 | SELECT * FROM SQL_STATEMENT; 48 | authRequired: 49 | - my-google-auth-service 50 | - other-auth-service 51 | parameters: 52 | - name: country 53 | type: string 54 | description: some description 55 | authServices: 56 | - name: my-google-auth-service 57 | field: user_id 58 | - name: other-auth-service 59 | field: user_id 60 | `, 61 | want: server.ToolConfigs{ 62 | "example_tool": mssqlsql.Config{ 63 | Name: "example_tool", 64 | Kind: "mssql-sql", 65 | Source: "my-instance", 66 | Description: "some description", 67 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 68 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 69 | Parameters: []tools.Parameter{ 70 | tools.NewStringParameterWithAuth("country", "some description", 71 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 72 | {Name: "other-auth-service", Field: "user_id"}}), 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | for _, tc := range tcs { 79 | t.Run(tc.desc, func(t *testing.T) { 80 | got := struct { 81 | Tools server.ToolConfigs `yaml:"tools"` 82 | }{} 83 | // Parse contents 84 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 85 | if err != nil { 86 | t.Fatalf("unable to unmarshal: %s", err) 87 | } 88 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 89 | t.Fatalf("incorrect parse: diff %v", diff) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func TestParseFromYamlWithTemplateMssql(t *testing.T) { 96 | ctx, err := testutils.ContextWithNewLogger() 97 | if err != nil { 98 | t.Fatalf("unexpected error: %s", err) 99 | } 100 | tcs := []struct { 101 | desc string 102 | in string 103 | want server.ToolConfigs 104 | }{ 105 | { 106 | desc: "basic example", 107 | in: ` 108 | tools: 109 | example_tool: 110 | kind: mssql-sql 111 | source: my-instance 112 | description: some description 113 | statement: | 114 | SELECT * FROM SQL_STATEMENT; 115 | authRequired: 116 | - my-google-auth-service 117 | - other-auth-service 118 | parameters: 119 | - name: country 120 | type: string 121 | description: some description 122 | authServices: 123 | - name: my-google-auth-service 124 | field: user_id 125 | - name: other-auth-service 126 | field: user_id 127 | templateParameters: 128 | - name: tableName 129 | type: string 130 | description: The table to select hotels from. 131 | - name: fieldArray 132 | type: array 133 | description: The columns to return for the query. 134 | items: 135 | name: column 136 | type: string 137 | description: A column name that will be returned from the query. 138 | `, 139 | want: server.ToolConfigs{ 140 | "example_tool": mssqlsql.Config{ 141 | Name: "example_tool", 142 | Kind: "mssql-sql", 143 | Source: "my-instance", 144 | Description: "some description", 145 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 146 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 147 | Parameters: []tools.Parameter{ 148 | tools.NewStringParameterWithAuth("country", "some description", 149 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 150 | {Name: "other-auth-service", Field: "user_id"}}), 151 | }, 152 | TemplateParameters: []tools.Parameter{ 153 | tools.NewStringParameter("tableName", "The table to select hotels from."), 154 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 155 | }, 156 | }, 157 | }, 158 | }, 159 | } 160 | for _, tc := range tcs { 161 | t.Run(tc.desc, func(t *testing.T) { 162 | got := struct { 163 | Tools server.ToolConfigs `yaml:"tools"` 164 | }{} 165 | // Parse contents 166 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 167 | if err != nil { 168 | t.Fatalf("unable to unmarshal: %s", err) 169 | } 170 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 171 | t.Fatalf("incorrect parse: diff %v", diff) 172 | } 173 | }) 174 | } 175 | } 176 | ``` -------------------------------------------------------------------------------- /tests/mysql/mysql_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 mysql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "os" 22 | "regexp" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | "github.com/google/uuid" 28 | "github.com/googleapis/genai-toolbox/internal/testutils" 29 | "github.com/googleapis/genai-toolbox/tests" 30 | ) 31 | 32 | var ( 33 | MySQLSourceKind = "mysql" 34 | MySQLToolKind = "mysql-sql" 35 | MySQLDatabase = os.Getenv("MYSQL_DATABASE") 36 | MySQLHost = os.Getenv("MYSQL_HOST") 37 | MySQLPort = os.Getenv("MYSQL_PORT") 38 | MySQLUser = os.Getenv("MYSQL_USER") 39 | MySQLPass = os.Getenv("MYSQL_PASS") 40 | ) 41 | 42 | func getMySQLVars(t *testing.T) map[string]any { 43 | switch "" { 44 | case MySQLDatabase: 45 | t.Fatal("'MYSQL_DATABASE' not set") 46 | case MySQLHost: 47 | t.Fatal("'MYSQL_HOST' not set") 48 | case MySQLPort: 49 | t.Fatal("'MYSQL_PORT' not set") 50 | case MySQLUser: 51 | t.Fatal("'MYSQL_USER' not set") 52 | case MySQLPass: 53 | t.Fatal("'MYSQL_PASS' not set") 54 | } 55 | 56 | return map[string]any{ 57 | "kind": MySQLSourceKind, 58 | "host": MySQLHost, 59 | "port": MySQLPort, 60 | "database": MySQLDatabase, 61 | "user": MySQLUser, 62 | "password": MySQLPass, 63 | } 64 | } 65 | 66 | // Copied over from mysql.go 67 | func initMySQLConnectionPool(host, port, user, pass, dbname string) (*sql.DB, error) { 68 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname) 69 | 70 | // Interact with the driver directly as you normally would 71 | pool, err := sql.Open("mysql", dsn) 72 | if err != nil { 73 | return nil, fmt.Errorf("sql.Open: %w", err) 74 | } 75 | return pool, nil 76 | } 77 | 78 | func TestMySQLToolEndpoints(t *testing.T) { 79 | sourceConfig := getMySQLVars(t) 80 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 81 | defer cancel() 82 | 83 | var args []string 84 | 85 | pool, err := initMySQLConnectionPool(MySQLHost, MySQLPort, MySQLUser, MySQLPass, MySQLDatabase) 86 | if err != nil { 87 | t.Fatalf("unable to create MySQL connection pool: %s", err) 88 | } 89 | 90 | // cleanup test environment 91 | tests.CleanupMySQLTables(t, ctx, pool) 92 | 93 | // create table name with UUID 94 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 95 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 96 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 97 | 98 | // set up data for param tool 99 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetMySQLParamToolInfo(tableNameParam) 100 | teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 101 | defer teardownTable1(t) 102 | 103 | // set up data for auth tool 104 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMySQLAuthToolInfo(tableNameAuth) 105 | teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 106 | defer teardownTable2(t) 107 | 108 | // Write config into a file and pass it to command 109 | toolsFile := tests.GetToolsConfig(sourceConfig, MySQLToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 110 | toolsFile = tests.AddMySqlExecuteSqlConfig(t, toolsFile) 111 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement() 112 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MySQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") 113 | 114 | toolsFile = tests.AddMySQLPrebuiltToolConfig(t, toolsFile) 115 | 116 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 117 | if err != nil { 118 | t.Fatalf("command initialization returned an error: %s", err) 119 | } 120 | defer cleanup() 121 | 122 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 123 | defer cancel() 124 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 125 | if err != nil { 126 | t.Logf("toolbox command logs: \n%s", out) 127 | t.Fatalf("toolbox didn't start successfully: %s", err) 128 | } 129 | 130 | // Get configs for tests 131 | select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMySQLWants() 132 | 133 | // Run tests 134 | tests.RunToolGetTest(t) 135 | tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest()) 136 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 137 | tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) 138 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 139 | 140 | // Run specific MySQL tool tests 141 | tests.RunMySQLListTablesTest(t, MySQLDatabase, tableNameParam, tableNameAuth) 142 | tests.RunMySQLListActiveQueriesTest(t, ctx, pool) 143 | tests.RunMySQLListTablesMissingUniqueIndexes(t, ctx, pool, MySQLDatabase) 144 | tests.RunMySQLListTableFragmentationTest(t, MySQLDatabase, tableNameParam, tableNameAuth) 145 | } 146 | ``` -------------------------------------------------------------------------------- /internal/tools/firebird/firebirdexecutesql/firebirdexecutesql.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 firebirdexecutesql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/firebird" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/util" 27 | ) 28 | 29 | const kind string = "firebird-execute-sql" 30 | 31 | func init() { 32 | if !tools.Register(kind, newConfig) { 33 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 34 | } 35 | } 36 | 37 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 38 | actual := Config{Name: name} 39 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 40 | return nil, err 41 | } 42 | return actual, nil 43 | } 44 | 45 | type compatibleSource interface { 46 | FirebirdDB() *sql.DB 47 | } 48 | 49 | var _ compatibleSource = &firebird.Source{} 50 | 51 | var compatibleSources = [...]string{firebird.SourceKind} 52 | 53 | type Config struct { 54 | Name string `yaml:"name" validate:"required"` 55 | Kind string `yaml:"kind" validate:"required"` 56 | Source string `yaml:"source" validate:"required"` 57 | Description string `yaml:"description" validate:"required"` 58 | AuthRequired []string `yaml:"authRequired"` 59 | } 60 | 61 | var _ tools.ToolConfig = Config{} 62 | 63 | func (cfg Config) ToolConfigKind() string { 64 | return kind 65 | } 66 | 67 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 68 | rawS, ok := srcs[cfg.Source] 69 | if !ok { 70 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 71 | } 72 | 73 | s, ok := rawS.(compatibleSource) 74 | if !ok { 75 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 76 | } 77 | 78 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 79 | parameters := tools.Parameters{sqlParameter} 80 | 81 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 82 | 83 | t := &Tool{ 84 | Name: cfg.Name, 85 | Parameters: parameters, 86 | AuthRequired: cfg.AuthRequired, 87 | Db: s.FirebirdDB(), 88 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 89 | mcpManifest: mcpManifest, 90 | } 91 | return t, nil 92 | } 93 | 94 | var _ tools.Tool = &Tool{} 95 | 96 | type Tool struct { 97 | Name string `yaml:"name"` 98 | Kind string `yaml:"kind"` 99 | AuthRequired []string `yaml:"authRequired"` 100 | Parameters tools.Parameters `yaml:"parameters"` 101 | 102 | Db *sql.DB 103 | manifest tools.Manifest 104 | mcpManifest tools.McpManifest 105 | } 106 | 107 | func (t *Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 108 | paramsMap := params.AsMap() 109 | sql, ok := paramsMap["sql"].(string) 110 | if !ok { 111 | return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"]) 112 | } 113 | 114 | // Log the query executed for debugging. 115 | logger, err := util.LoggerFromContext(ctx) 116 | if err != nil { 117 | return nil, fmt.Errorf("error getting logger: %s", err) 118 | } 119 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql) 120 | 121 | rows, err := t.Db.QueryContext(ctx, sql) 122 | if err != nil { 123 | return nil, fmt.Errorf("unable to execute query: %w", err) 124 | } 125 | defer rows.Close() 126 | 127 | cols, err := rows.Columns() 128 | 129 | var out []any 130 | if err == nil && len(cols) > 0 { 131 | values := make([]any, len(cols)) 132 | scanArgs := make([]any, len(values)) 133 | for i := range values { 134 | scanArgs[i] = &values[i] 135 | } 136 | 137 | for rows.Next() { 138 | err = rows.Scan(scanArgs...) 139 | if err != nil { 140 | return nil, fmt.Errorf("unable to parse row: %w", err) 141 | } 142 | 143 | vMap := make(map[string]any) 144 | for i, colName := range cols { 145 | if b, ok := values[i].([]byte); ok { 146 | vMap[colName] = string(b) 147 | } else { 148 | vMap[colName] = values[i] 149 | } 150 | } 151 | out = append(out, vMap) 152 | } 153 | } 154 | 155 | if err := rows.Err(); err != nil { 156 | return nil, fmt.Errorf("error iterating rows: %w", err) 157 | } 158 | 159 | // In most cases, DML/DDL statements like INSERT, UPDATE, CREATE, etc. might return no rows 160 | // However, it is also possible that this was a query that was expected to return rows 161 | // but returned none, a case that we cannot distinguish here. 162 | return out, nil 163 | } 164 | 165 | func (t *Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 166 | return tools.ParseParams(t.Parameters, data, claims) 167 | } 168 | 169 | func (t *Tool) Manifest() tools.Manifest { 170 | return t.manifest 171 | } 172 | 173 | func (t *Tool) McpManifest() tools.McpManifest { 174 | return t.mcpManifest 175 | } 176 | 177 | func (t *Tool) Authorized(verifiedAuthServices []string) bool { 178 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 179 | } 180 | 181 | func (t Tool) RequiresClientAuthorization() bool { 182 | return false 183 | } 184 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetfilters/lookergetfilters.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package lookergetfilters 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/googleapis/genai-toolbox/internal/sources" 22 | lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker" 23 | "github.com/googleapis/genai-toolbox/internal/tools" 24 | "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" 25 | "github.com/googleapis/genai-toolbox/internal/util" 26 | 27 | "github.com/looker-open-source/sdk-codegen/go/rtl" 28 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 29 | ) 30 | 31 | const kind string = "looker-get-filters" 32 | 33 | func init() { 34 | if !tools.Register(kind, newConfig) { 35 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 36 | } 37 | } 38 | 39 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 40 | actual := Config{Name: name} 41 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 42 | return nil, err 43 | } 44 | return actual, nil 45 | } 46 | 47 | type Config struct { 48 | Name string `yaml:"name" validate:"required"` 49 | Kind string `yaml:"kind" validate:"required"` 50 | Source string `yaml:"source" validate:"required"` 51 | Description string `yaml:"description" validate:"required"` 52 | AuthRequired []string `yaml:"authRequired"` 53 | } 54 | 55 | // validate interface 56 | var _ tools.ToolConfig = Config{} 57 | 58 | func (cfg Config) ToolConfigKind() string { 59 | return kind 60 | } 61 | 62 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 63 | // verify source exists 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | 69 | // verify the source is compatible 70 | s, ok := rawS.(*lookersrc.Source) 71 | if !ok { 72 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 73 | } 74 | 75 | parameters := lookercommon.GetFieldParameters() 76 | 77 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 78 | 79 | // finish tool setup 80 | return Tool{ 81 | Name: cfg.Name, 82 | Kind: kind, 83 | Parameters: parameters, 84 | AuthRequired: cfg.AuthRequired, 85 | UseClientOAuth: s.UseClientOAuth, 86 | Client: s.Client, 87 | ApiSettings: s.ApiSettings, 88 | manifest: tools.Manifest{ 89 | Description: cfg.Description, 90 | Parameters: parameters.Manifest(), 91 | AuthRequired: cfg.AuthRequired, 92 | }, 93 | mcpManifest: mcpManifest, 94 | ShowHiddenFields: s.ShowHiddenFields, 95 | }, nil 96 | } 97 | 98 | // validate interface 99 | var _ tools.Tool = Tool{} 100 | 101 | type Tool struct { 102 | Name string `yaml:"name"` 103 | Kind string `yaml:"kind"` 104 | UseClientOAuth bool 105 | Client *v4.LookerSDK 106 | ApiSettings *rtl.ApiSettings 107 | AuthRequired []string `yaml:"authRequired"` 108 | Parameters tools.Parameters `yaml:"parameters"` 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | ShowHiddenFields bool 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | logger, err := util.LoggerFromContext(ctx) 116 | if err != nil { 117 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 118 | } 119 | model, explore, err := lookercommon.ProcessFieldArgs(ctx, params) 120 | if err != nil { 121 | return nil, fmt.Errorf("error processing model or explore: %w", err) 122 | } 123 | 124 | fields := lookercommon.FiltersFields 125 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 126 | if err != nil { 127 | return nil, fmt.Errorf("error getting sdk: %w", err) 128 | } 129 | req := v4.RequestLookmlModelExplore{ 130 | LookmlModelName: *model, 131 | ExploreName: *explore, 132 | Fields: &fields, 133 | } 134 | resp, err := sdk.LookmlModelExplore(req, t.ApiSettings) 135 | if err != nil { 136 | return nil, fmt.Errorf("error making get_filters request: %w", err) 137 | } 138 | 139 | if err := lookercommon.CheckLookerExploreFields(&resp); err != nil { 140 | return nil, fmt.Errorf("error processing get_filters response: %w", err) 141 | } 142 | 143 | data, err := lookercommon.ExtractLookerFieldProperties(ctx, resp.Fields.Filters, t.ShowHiddenFields) 144 | if err != nil { 145 | return nil, fmt.Errorf("error extracting get_filters response: %w", err) 146 | } 147 | logger.DebugContext(ctx, "data = ", data) 148 | 149 | return data, nil 150 | } 151 | 152 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 153 | return tools.ParseParams(t.Parameters, data, claims) 154 | } 155 | 156 | func (t Tool) Manifest() tools.Manifest { 157 | return t.manifest 158 | } 159 | 160 | func (t Tool) McpManifest() tools.McpManifest { 161 | return t.mcpManifest 162 | } 163 | 164 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 165 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 166 | } 167 | 168 | func (t Tool) RequiresClientAuthorization() bool { 169 | return t.UseClientOAuth 170 | } 171 | ``` -------------------------------------------------------------------------------- /internal/tools/tidb/tidbsql/tidbsql_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 tidbsql_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/tidb/tidbsql" 26 | ) 27 | 28 | func TestParseFromYamlTiDB(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: tidb-sql 44 | source: my-tidb-instance 45 | description: some description 46 | statement: | 47 | SELECT * FROM SQL_STATEMENT; 48 | authRequired: 49 | - my-google-auth-service 50 | - other-auth-service 51 | parameters: 52 | - name: country 53 | type: string 54 | description: some description 55 | authServices: 56 | - name: my-google-auth-service 57 | field: user_id 58 | - name: other-auth-service 59 | field: user_id 60 | `, 61 | want: server.ToolConfigs{ 62 | "example_tool": tidbsql.Config{ 63 | Name: "example_tool", 64 | Kind: "tidb-sql", 65 | Source: "my-tidb-instance", 66 | Description: "some description", 67 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 68 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 69 | Parameters: []tools.Parameter{ 70 | tools.NewStringParameterWithAuth("country", "some description", 71 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 72 | {Name: "other-auth-service", Field: "user_id"}}), 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | for _, tc := range tcs { 79 | t.Run(tc.desc, func(t *testing.T) { 80 | got := struct { 81 | Tools server.ToolConfigs `yaml:"tools"` 82 | }{} 83 | // Parse contents 84 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 85 | if err != nil { 86 | t.Fatalf("unable to unmarshal: %s", err) 87 | } 88 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 89 | t.Fatalf("incorrect parse: diff %v", diff) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func TestParseFromYamlWithTemplateParamsTiDB(t *testing.T) { 96 | ctx, err := testutils.ContextWithNewLogger() 97 | if err != nil { 98 | t.Fatalf("unexpected error: %s", err) 99 | } 100 | tcs := []struct { 101 | desc string 102 | in string 103 | want server.ToolConfigs 104 | }{ 105 | { 106 | desc: "basic example", 107 | in: ` 108 | tools: 109 | example_tool: 110 | kind: tidb-sql 111 | source: my-tidb-instance 112 | description: some description 113 | statement: | 114 | SELECT * FROM SQL_STATEMENT; 115 | authRequired: 116 | - my-google-auth-service 117 | - other-auth-service 118 | parameters: 119 | - name: country 120 | type: string 121 | description: some description 122 | authServices: 123 | - name: my-google-auth-service 124 | field: user_id 125 | - name: other-auth-service 126 | field: user_id 127 | templateParameters: 128 | - name: tableName 129 | type: string 130 | description: The table to select hotels from. 131 | - name: fieldArray 132 | type: array 133 | description: The columns to return for the query. 134 | items: 135 | name: column 136 | type: string 137 | description: A column name that will be returned from the query. 138 | `, 139 | want: server.ToolConfigs{ 140 | "example_tool": tidbsql.Config{ 141 | Name: "example_tool", 142 | Kind: "tidb-sql", 143 | Source: "my-tidb-instance", 144 | Description: "some description", 145 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 146 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 147 | Parameters: []tools.Parameter{ 148 | tools.NewStringParameterWithAuth("country", "some description", 149 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 150 | {Name: "other-auth-service", Field: "user_id"}}), 151 | }, 152 | TemplateParameters: []tools.Parameter{ 153 | tools.NewStringParameter("tableName", "The table to select hotels from."), 154 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 155 | }, 156 | }, 157 | }, 158 | }, 159 | } 160 | for _, tc := range tcs { 161 | t.Run(tc.desc, func(t *testing.T) { 162 | got := struct { 163 | Tools server.ToolConfigs `yaml:"tools"` 164 | }{} 165 | // Parse contents 166 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 167 | if err != nil { 168 | t.Fatalf("unable to unmarshal: %s", err) 169 | } 170 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 171 | t.Fatalf("incorrect parse: diff %v", diff) 172 | } 173 | }) 174 | } 175 | } 176 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/postgres/postgres-sql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "postgres-sql" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "postgres-sql" tool executes a pre-defined SQL statement against a Postgres 7 | database. 8 | aliases: 9 | - /resources/tools/postgres-sql 10 | --- 11 | 12 | ## About 13 | 14 | A `postgres-sql` tool executes a pre-defined SQL statement against a Postgres 15 | database. It's compatible with any of the following sources: 16 | 17 | - [alloydb-postgres](../../sources/alloydb-pg.md) 18 | - [cloud-sql-postgres](../../sources/cloud-sql-pg.md) 19 | - [postgres](../../sources/postgres.md) 20 | 21 | The specified SQL statement is executed as a [prepared statement][pg-prepare], 22 | and specified parameters will be inserted according to their position: e.g. `$1` 23 | will be the first parameter specified, `$2` will be the second parameter, and so 24 | on. If template parameters are included, they will be resolved before execution 25 | of the prepared statement. 26 | 27 | [pg-prepare]: https://www.postgresql.org/docs/current/sql-prepare.html 28 | 29 | ## Example 30 | 31 | > **Note:** This tool uses parameterized queries to prevent SQL injections. 32 | > Query parameters can be used as substitutes for arbitrary expressions. 33 | > Parameters cannot be used as substitutes for identifiers, column names, table 34 | > names, or other parts of the query. 35 | 36 | ```yaml 37 | tools: 38 | search_flights_by_number: 39 | kind: postgres-sql 40 | source: my-pg-instance 41 | statement: | 42 | SELECT * FROM flights 43 | WHERE airline = $1 44 | AND flight_number = $2 45 | LIMIT 10 46 | description: | 47 | Use this tool to get information for a specific flight. 48 | Takes an airline code and flight number and returns info on the flight. 49 | Do NOT use this tool with a flight id. Do NOT guess an airline code or flight number. 50 | A airline code is a code for an airline service consisting of two-character 51 | airline designator and followed by flight number, which is 1 to 4 digit number. 52 | For example, if given CY 0123, the airline is "CY", and flight_number is "123". 53 | Another example for this is DL 1234, the airline is "DL", and flight_number is "1234". 54 | If the tool returns more than one option choose the date closes to today. 55 | Example: 56 | {{ 57 | "airline": "CY", 58 | "flight_number": "888", 59 | }} 60 | Example: 61 | {{ 62 | "airline": "DL", 63 | "flight_number": "1234", 64 | }} 65 | parameters: 66 | - name: airline 67 | type: string 68 | description: Airline unique 2 letter identifier 69 | - name: flight_number 70 | type: string 71 | description: 1 to 4 digit number 72 | ``` 73 | 74 | ### Example with Template Parameters 75 | 76 | > **Note:** This tool allows direct modifications to the SQL statement, 77 | > including identifiers, column names, and table names. **This makes it more 78 | > vulnerable to SQL injections**. Using basic parameters only (see above) is 79 | > recommended for performance and safety reasons. For more details, please check 80 | > [templateParameters](..#template-parameters). 81 | 82 | ```yaml 83 | tools: 84 | list_table: 85 | kind: postgres-sql 86 | source: my-pg-instance 87 | statement: | 88 | SELECT * FROM {{.tableName}} 89 | description: | 90 | Use this tool to list all information from a specific table. 91 | Example: 92 | {{ 93 | "tableName": "flights", 94 | }} 95 | templateParameters: 96 | - name: tableName 97 | type: string 98 | description: Table to select from 99 | ``` 100 | 101 | ## Reference 102 | 103 | | **field** | **type** | **required** | **description** | 104 | |---------------------|:---------------------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------| 105 | | kind | string | true | Must be "postgres-sql". | 106 | | source | string | true | Name of the source the SQL should execute on. | 107 | | description | string | true | Description of the tool that is passed to the LLM. | 108 | | statement | string | true | SQL statement to execute on. | 109 | | parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | 110 | | templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | 111 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetmeasures/lookergetmeasures.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package lookergetmeasures 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/googleapis/genai-toolbox/internal/sources" 22 | lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker" 23 | "github.com/googleapis/genai-toolbox/internal/tools" 24 | "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" 25 | "github.com/googleapis/genai-toolbox/internal/util" 26 | 27 | "github.com/looker-open-source/sdk-codegen/go/rtl" 28 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 29 | ) 30 | 31 | const kind string = "looker-get-measures" 32 | 33 | func init() { 34 | if !tools.Register(kind, newConfig) { 35 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 36 | } 37 | } 38 | 39 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 40 | actual := Config{Name: name} 41 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 42 | return nil, err 43 | } 44 | return actual, nil 45 | } 46 | 47 | type Config struct { 48 | Name string `yaml:"name" validate:"required"` 49 | Kind string `yaml:"kind" validate:"required"` 50 | Source string `yaml:"source" validate:"required"` 51 | Description string `yaml:"description" validate:"required"` 52 | AuthRequired []string `yaml:"authRequired"` 53 | } 54 | 55 | // validate interface 56 | var _ tools.ToolConfig = Config{} 57 | 58 | func (cfg Config) ToolConfigKind() string { 59 | return kind 60 | } 61 | 62 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 63 | // verify source exists 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | 69 | // verify the source is compatible 70 | s, ok := rawS.(*lookersrc.Source) 71 | if !ok { 72 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 73 | } 74 | 75 | parameters := lookercommon.GetFieldParameters() 76 | 77 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 78 | 79 | // finish tool setup 80 | return Tool{ 81 | Name: cfg.Name, 82 | Kind: kind, 83 | Parameters: parameters, 84 | AuthRequired: cfg.AuthRequired, 85 | UseClientOAuth: s.UseClientOAuth, 86 | Client: s.Client, 87 | ApiSettings: s.ApiSettings, 88 | manifest: tools.Manifest{ 89 | Description: cfg.Description, 90 | Parameters: parameters.Manifest(), 91 | AuthRequired: cfg.AuthRequired, 92 | }, 93 | mcpManifest: mcpManifest, 94 | ShowHiddenFields: s.ShowHiddenFields, 95 | }, nil 96 | } 97 | 98 | // validate interface 99 | var _ tools.Tool = Tool{} 100 | 101 | type Tool struct { 102 | Name string `yaml:"name"` 103 | Kind string `yaml:"kind"` 104 | UseClientOAuth bool 105 | Client *v4.LookerSDK 106 | ApiSettings *rtl.ApiSettings 107 | AuthRequired []string `yaml:"authRequired"` 108 | Parameters tools.Parameters `yaml:"parameters"` 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | ShowHiddenFields bool 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | logger, err := util.LoggerFromContext(ctx) 116 | if err != nil { 117 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 118 | } 119 | model, explore, err := lookercommon.ProcessFieldArgs(ctx, params) 120 | if err != nil { 121 | return nil, fmt.Errorf("error processing model or explore: %w", err) 122 | } 123 | 124 | fields := lookercommon.MeasuresFields 125 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 126 | if err != nil { 127 | return nil, fmt.Errorf("error getting sdk: %w", err) 128 | } 129 | req := v4.RequestLookmlModelExplore{ 130 | LookmlModelName: *model, 131 | ExploreName: *explore, 132 | Fields: &fields, 133 | } 134 | resp, err := sdk.LookmlModelExplore(req, t.ApiSettings) 135 | if err != nil { 136 | return nil, fmt.Errorf("error making get_measures request: %w", err) 137 | } 138 | 139 | if err := lookercommon.CheckLookerExploreFields(&resp); err != nil { 140 | return nil, fmt.Errorf("error processing get_measures response: %w", err) 141 | } 142 | 143 | data, err := lookercommon.ExtractLookerFieldProperties(ctx, resp.Fields.Measures, t.ShowHiddenFields) 144 | if err != nil { 145 | return nil, fmt.Errorf("error extracting get_measures response: %w", err) 146 | } 147 | logger.DebugContext(ctx, "data = ", data) 148 | 149 | return data, nil 150 | } 151 | 152 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 153 | return tools.ParseParams(t.Parameters, data, claims) 154 | } 155 | 156 | func (t Tool) Manifest() tools.Manifest { 157 | return t.manifest 158 | } 159 | 160 | func (t Tool) McpManifest() tools.McpManifest { 161 | return t.mcpManifest 162 | } 163 | 164 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 165 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 166 | } 167 | 168 | func (t Tool) RequiresClientAuthorization() bool { 169 | return t.UseClientOAuth 170 | } 171 | ``` -------------------------------------------------------------------------------- /internal/tools/mysql/mysqlsql/mysqlsql_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mysqlsql_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlsql" 26 | ) 27 | 28 | func TestParseFromYamlMySQL(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: mysql-sql 44 | source: my-mysql-instance 45 | description: some description 46 | statement: | 47 | SELECT * FROM SQL_STATEMENT; 48 | authRequired: 49 | - my-google-auth-service 50 | - other-auth-service 51 | parameters: 52 | - name: country 53 | type: string 54 | description: some description 55 | authServices: 56 | - name: my-google-auth-service 57 | field: user_id 58 | - name: other-auth-service 59 | field: user_id 60 | `, 61 | want: server.ToolConfigs{ 62 | "example_tool": mysqlsql.Config{ 63 | Name: "example_tool", 64 | Kind: "mysql-sql", 65 | Source: "my-mysql-instance", 66 | Description: "some description", 67 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 68 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 69 | Parameters: []tools.Parameter{ 70 | tools.NewStringParameterWithAuth("country", "some description", 71 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 72 | {Name: "other-auth-service", Field: "user_id"}}), 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | for _, tc := range tcs { 79 | t.Run(tc.desc, func(t *testing.T) { 80 | got := struct { 81 | Tools server.ToolConfigs `yaml:"tools"` 82 | }{} 83 | // Parse contents 84 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 85 | if err != nil { 86 | t.Fatalf("unable to unmarshal: %s", err) 87 | } 88 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 89 | t.Fatalf("incorrect parse: diff %v", diff) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func TestParseFromYamlWithTemplateParamsMySQL(t *testing.T) { 96 | ctx, err := testutils.ContextWithNewLogger() 97 | if err != nil { 98 | t.Fatalf("unexpected error: %s", err) 99 | } 100 | tcs := []struct { 101 | desc string 102 | in string 103 | want server.ToolConfigs 104 | }{ 105 | { 106 | desc: "basic example", 107 | in: ` 108 | tools: 109 | example_tool: 110 | kind: mysql-sql 111 | source: my-mysql-instance 112 | description: some description 113 | statement: | 114 | SELECT * FROM SQL_STATEMENT; 115 | authRequired: 116 | - my-google-auth-service 117 | - other-auth-service 118 | parameters: 119 | - name: country 120 | type: string 121 | description: some description 122 | authServices: 123 | - name: my-google-auth-service 124 | field: user_id 125 | - name: other-auth-service 126 | field: user_id 127 | templateParameters: 128 | - name: tableName 129 | type: string 130 | description: The table to select hotels from. 131 | - name: fieldArray 132 | type: array 133 | description: The columns to return for the query. 134 | items: 135 | name: column 136 | type: string 137 | description: A column name that will be returned from the query. 138 | `, 139 | want: server.ToolConfigs{ 140 | "example_tool": mysqlsql.Config{ 141 | Name: "example_tool", 142 | Kind: "mysql-sql", 143 | Source: "my-mysql-instance", 144 | Description: "some description", 145 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 146 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 147 | Parameters: []tools.Parameter{ 148 | tools.NewStringParameterWithAuth("country", "some description", 149 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 150 | {Name: "other-auth-service", Field: "user_id"}}), 151 | }, 152 | TemplateParameters: []tools.Parameter{ 153 | tools.NewStringParameter("tableName", "The table to select hotels from."), 154 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 155 | }, 156 | }, 157 | }, 158 | }, 159 | } 160 | for _, tc := range tcs { 161 | t.Run(tc.desc, func(t *testing.T) { 162 | got := struct { 163 | Tools server.ToolConfigs `yaml:"tools"` 164 | }{} 165 | // Parse contents 166 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 167 | if err != nil { 168 | t.Fatalf("unable to unmarshal: %s", err) 169 | } 170 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 171 | t.Fatalf("incorrect parse: diff %v", diff) 172 | } 173 | }) 174 | } 175 | } 176 | ``` -------------------------------------------------------------------------------- /internal/tools/alloydb/alloydblistinstances/alloydblistinstances.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 alloydblistinstances 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | alloydbadmin "github.com/googleapis/genai-toolbox/internal/sources/alloydbadmin" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | ) 26 | 27 | const kind string = "alloydb-list-instances" 28 | 29 | func init() { 30 | if !tools.Register(kind, newConfig) { 31 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 32 | } 33 | } 34 | 35 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 36 | actual := Config{Name: name} 37 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 38 | return nil, err 39 | } 40 | return actual, nil 41 | } 42 | 43 | // Configuration for the list-instances tool. 44 | type Config struct { 45 | Name string `yaml:"name" validate:"required"` 46 | Kind string `yaml:"kind" validate:"required"` 47 | Source string `yaml:"source" validate:"required"` 48 | Description string `yaml:"description"` 49 | AuthRequired []string `yaml:"authRequired"` 50 | BaseURL string `yaml:"baseURL"` 51 | } 52 | 53 | // validate interface 54 | var _ tools.ToolConfig = Config{} 55 | 56 | // ToolConfigKind returns the kind of the tool. 57 | func (cfg Config) ToolConfigKind() string { 58 | return kind 59 | } 60 | 61 | // Initialize initializes the tool from the configuration. 62 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 63 | rawS, ok := srcs[cfg.Source] 64 | if !ok { 65 | return nil, fmt.Errorf("source %q not found", cfg.Source) 66 | } 67 | 68 | s, ok := rawS.(*alloydbadmin.Source) 69 | if !ok { 70 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `%s`", kind, alloydbadmin.SourceKind) 71 | } 72 | 73 | allParameters := tools.Parameters{ 74 | tools.NewStringParameter("project", "The GCP project ID to list instances for."), 75 | tools.NewStringParameterWithDefault("location", "-", "Optional: The location of the cluster (e.g., 'us-central1'). Use '-' to get results for all regions.(Default: '-')"), 76 | tools.NewStringParameterWithDefault("cluster", "-", "Optional: The ID of the cluster to list instances from. Use '-' to get results for all clusters.(Default: '-')"), 77 | } 78 | paramManifest := allParameters.Manifest() 79 | 80 | description := cfg.Description 81 | if description == "" { 82 | description = "Lists all AlloyDB instances in a given project, location and cluster." 83 | } 84 | mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters) 85 | 86 | return Tool{ 87 | Name: cfg.Name, 88 | Kind: kind, 89 | Source: s, 90 | AllParams: allParameters, 91 | manifest: tools.Manifest{Description: description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 92 | mcpManifest: mcpManifest, 93 | }, nil 94 | } 95 | 96 | // Tool represents the list-instances tool. 97 | type Tool struct { 98 | Name string `yaml:"name"` 99 | Kind string `yaml:"kind"` 100 | Description string `yaml:"description"` 101 | 102 | Source *alloydbadmin.Source 103 | AllParams tools.Parameters `yaml:"allParams"` 104 | 105 | manifest tools.Manifest 106 | mcpManifest tools.McpManifest 107 | } 108 | 109 | // Invoke executes the tool's logic. 110 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 111 | paramsMap := params.AsMap() 112 | 113 | project, ok := paramsMap["project"].(string) 114 | if !ok { 115 | return nil, fmt.Errorf("invalid or missing 'project' parameter; expected a string") 116 | } 117 | location, ok := paramsMap["location"].(string) 118 | if !ok { 119 | return nil, fmt.Errorf("invalid 'location' parameter; expected a string") 120 | } 121 | cluster, ok := paramsMap["cluster"].(string) 122 | if !ok { 123 | return nil, fmt.Errorf("invalid 'cluster' parameter; expected a string") 124 | } 125 | 126 | service, err := t.Source.GetService(ctx, string(accessToken)) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | urlString := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", project, location, cluster) 132 | 133 | resp, err := service.Projects.Locations.Clusters.Instances.List(urlString).Do() 134 | if err != nil { 135 | return nil, fmt.Errorf("error listing AlloyDB instances: %w", err) 136 | } 137 | 138 | return resp, nil 139 | } 140 | 141 | // ParseParams parses the parameters for the tool. 142 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 143 | return tools.ParseParams(t.AllParams, data, claims) 144 | } 145 | 146 | // Manifest returns the tool's manifest. 147 | func (t Tool) Manifest() tools.Manifest { 148 | return t.manifest 149 | } 150 | 151 | // McpManifest returns the tool's MCP manifest. 152 | func (t Tool) McpManifest() tools.McpManifest { 153 | return t.mcpManifest 154 | } 155 | 156 | // Authorized checks if the tool is authorized. 157 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 158 | return true 159 | } 160 | 161 | func (t Tool) RequiresClientAuthorization() bool { 162 | return t.Source.UseClientAuthorization() 163 | } 164 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetdimensions/lookergetdimensions.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package lookergetdimensions 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/googleapis/genai-toolbox/internal/sources" 22 | lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker" 23 | "github.com/googleapis/genai-toolbox/internal/tools" 24 | "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" 25 | "github.com/googleapis/genai-toolbox/internal/util" 26 | 27 | "github.com/looker-open-source/sdk-codegen/go/rtl" 28 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 29 | ) 30 | 31 | const kind string = "looker-get-dimensions" 32 | 33 | func init() { 34 | if !tools.Register(kind, newConfig) { 35 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 36 | } 37 | } 38 | 39 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 40 | actual := Config{Name: name} 41 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 42 | return nil, err 43 | } 44 | return actual, nil 45 | } 46 | 47 | type Config struct { 48 | Name string `yaml:"name" validate:"required"` 49 | Kind string `yaml:"kind" validate:"required"` 50 | Source string `yaml:"source" validate:"required"` 51 | Description string `yaml:"description" validate:"required"` 52 | AuthRequired []string `yaml:"authRequired"` 53 | } 54 | 55 | // validate interface 56 | var _ tools.ToolConfig = Config{} 57 | 58 | func (cfg Config) ToolConfigKind() string { 59 | return kind 60 | } 61 | 62 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 63 | // verify source exists 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | 69 | // verify the source is compatible 70 | s, ok := rawS.(*lookersrc.Source) 71 | if !ok { 72 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 73 | } 74 | 75 | parameters := lookercommon.GetFieldParameters() 76 | 77 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 78 | 79 | // finish tool setup 80 | return Tool{ 81 | Name: cfg.Name, 82 | Kind: kind, 83 | Parameters: parameters, 84 | UseClientOAuth: s.UseClientOAuth, 85 | Client: s.Client, 86 | AuthRequired: cfg.AuthRequired, 87 | ApiSettings: s.ApiSettings, 88 | manifest: tools.Manifest{ 89 | Description: cfg.Description, 90 | Parameters: parameters.Manifest(), 91 | AuthRequired: cfg.AuthRequired, 92 | }, 93 | mcpManifest: mcpManifest, 94 | ShowHiddenFields: s.ShowHiddenFields, 95 | }, nil 96 | } 97 | 98 | // validate interface 99 | var _ tools.Tool = Tool{} 100 | 101 | type Tool struct { 102 | Name string `yaml:"name"` 103 | Kind string `yaml:"kind"` 104 | UseClientOAuth bool 105 | Client *v4.LookerSDK 106 | ApiSettings *rtl.ApiSettings 107 | AuthRequired []string `yaml:"authRequired"` 108 | Parameters tools.Parameters `yaml:"parameters"` 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | ShowHiddenFields bool 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | logger, err := util.LoggerFromContext(ctx) 116 | if err != nil { 117 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 118 | } 119 | model, explore, err := lookercommon.ProcessFieldArgs(ctx, params) 120 | if err != nil { 121 | return nil, fmt.Errorf("error processing model or explore: %w", err) 122 | } 123 | 124 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 125 | if err != nil { 126 | return nil, fmt.Errorf("error getting sdk: %w", err) 127 | } 128 | fields := lookercommon.DimensionsFields 129 | req := v4.RequestLookmlModelExplore{ 130 | LookmlModelName: *model, 131 | ExploreName: *explore, 132 | Fields: &fields, 133 | } 134 | resp, err := sdk.LookmlModelExplore(req, t.ApiSettings) 135 | if err != nil { 136 | return nil, fmt.Errorf("error making get_dimensions request: %w", err) 137 | } 138 | 139 | if err := lookercommon.CheckLookerExploreFields(&resp); err != nil { 140 | return nil, fmt.Errorf("error processing get_dimensions response: %w", err) 141 | } 142 | 143 | data, err := lookercommon.ExtractLookerFieldProperties(ctx, resp.Fields.Dimensions, t.ShowHiddenFields) 144 | if err != nil { 145 | return nil, fmt.Errorf("error extracting get_dimensions response: %w", err) 146 | } 147 | logger.DebugContext(ctx, "data = ", data) 148 | 149 | return data, nil 150 | } 151 | 152 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 153 | return tools.ParseParams(t.Parameters, data, claims) 154 | } 155 | 156 | func (t Tool) Manifest() tools.Manifest { 157 | return t.manifest 158 | } 159 | 160 | func (t Tool) McpManifest() tools.McpManifest { 161 | return t.mcpManifest 162 | } 163 | 164 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 165 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 166 | } 167 | 168 | func (t Tool) RequiresClientAuthorization() bool { 169 | return t.UseClientOAuth 170 | } 171 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetparameters/lookergetparameters.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package lookergetparameters 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/googleapis/genai-toolbox/internal/sources" 22 | lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker" 23 | "github.com/googleapis/genai-toolbox/internal/tools" 24 | "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" 25 | "github.com/googleapis/genai-toolbox/internal/util" 26 | 27 | "github.com/looker-open-source/sdk-codegen/go/rtl" 28 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 29 | ) 30 | 31 | const kind string = "looker-get-parameters" 32 | 33 | func init() { 34 | if !tools.Register(kind, newConfig) { 35 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 36 | } 37 | } 38 | 39 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 40 | actual := Config{Name: name} 41 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 42 | return nil, err 43 | } 44 | return actual, nil 45 | } 46 | 47 | type Config struct { 48 | Name string `yaml:"name" validate:"required"` 49 | Kind string `yaml:"kind" validate:"required"` 50 | Source string `yaml:"source" validate:"required"` 51 | Description string `yaml:"description" validate:"required"` 52 | AuthRequired []string `yaml:"authRequired"` 53 | } 54 | 55 | // validate interface 56 | var _ tools.ToolConfig = Config{} 57 | 58 | func (cfg Config) ToolConfigKind() string { 59 | return kind 60 | } 61 | 62 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 63 | // verify source exists 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | 69 | // verify the source is compatible 70 | s, ok := rawS.(*lookersrc.Source) 71 | if !ok { 72 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 73 | } 74 | 75 | parameters := lookercommon.GetFieldParameters() 76 | 77 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 78 | 79 | // finish tool setup 80 | return Tool{ 81 | Name: cfg.Name, 82 | Kind: kind, 83 | Parameters: parameters, 84 | AuthRequired: cfg.AuthRequired, 85 | UseClientOAuth: s.UseClientOAuth, 86 | Client: s.Client, 87 | ApiSettings: s.ApiSettings, 88 | manifest: tools.Manifest{ 89 | Description: cfg.Description, 90 | Parameters: parameters.Manifest(), 91 | AuthRequired: cfg.AuthRequired, 92 | }, 93 | mcpManifest: mcpManifest, 94 | ShowHiddenFields: s.ShowHiddenFields, 95 | }, nil 96 | } 97 | 98 | // validate interface 99 | var _ tools.Tool = Tool{} 100 | 101 | type Tool struct { 102 | Name string `yaml:"name"` 103 | Kind string `yaml:"kind"` 104 | UseClientOAuth bool 105 | Client *v4.LookerSDK 106 | ApiSettings *rtl.ApiSettings 107 | AuthRequired []string `yaml:"authRequired"` 108 | Parameters tools.Parameters `yaml:"parameters"` 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | ShowHiddenFields bool 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | logger, err := util.LoggerFromContext(ctx) 116 | if err != nil { 117 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 118 | } 119 | model, explore, err := lookercommon.ProcessFieldArgs(ctx, params) 120 | if err != nil { 121 | return nil, fmt.Errorf("error processing model or explore: %w", err) 122 | } 123 | 124 | fields := lookercommon.ParametersFields 125 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 126 | if err != nil { 127 | return nil, fmt.Errorf("error getting sdk: %w", err) 128 | } 129 | req := v4.RequestLookmlModelExplore{ 130 | LookmlModelName: *model, 131 | ExploreName: *explore, 132 | Fields: &fields, 133 | } 134 | resp, err := sdk.LookmlModelExplore(req, t.ApiSettings) 135 | if err != nil { 136 | return nil, fmt.Errorf("error making get_parameters request: %w", err) 137 | } 138 | 139 | if err := lookercommon.CheckLookerExploreFields(&resp); err != nil { 140 | return nil, fmt.Errorf("error processing get_parameters response: %w", err) 141 | } 142 | 143 | data, err := lookercommon.ExtractLookerFieldProperties(ctx, resp.Fields.Parameters, t.ShowHiddenFields) 144 | if err != nil { 145 | return nil, fmt.Errorf("error extracting get_parameters response: %w", err) 146 | } 147 | logger.DebugContext(ctx, "data = ", data) 148 | 149 | return data, nil 150 | } 151 | 152 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 153 | return tools.ParseParams(t.Parameters, data, claims) 154 | } 155 | 156 | func (t Tool) Manifest() tools.Manifest { 157 | return t.manifest 158 | } 159 | 160 | func (t Tool) McpManifest() tools.McpManifest { 161 | return t.mcpManifest 162 | } 163 | 164 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 165 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 166 | } 167 | 168 | func (t Tool) RequiresClientAuthorization() bool { 169 | return t.UseClientOAuth 170 | } 171 | ``` -------------------------------------------------------------------------------- /internal/tools/cassandra/cassandracql/cassandracql_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 cassandracql_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/cassandra/cassandracql" 26 | ) 27 | 28 | func TestParseFromYamlCassandra(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: cassandra-cql 44 | source: my-cassandra-instance 45 | description: some description 46 | statement: | 47 | SELECT * FROM CQL_STATEMENT; 48 | authRequired: 49 | - my-google-auth-service 50 | - other-auth-service 51 | parameters: 52 | - name: country 53 | type: string 54 | description: some description 55 | authServices: 56 | - name: my-google-auth-service 57 | field: user_id 58 | - name: other-auth-service 59 | field: user_id 60 | `, 61 | want: server.ToolConfigs{ 62 | "example_tool": cassandracql.Config{ 63 | Name: "example_tool", 64 | Kind: "cassandra-cql", 65 | Source: "my-cassandra-instance", 66 | Description: "some description", 67 | Statement: "SELECT * FROM CQL_STATEMENT;\n", 68 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 69 | Parameters: []tools.Parameter{ 70 | tools.NewStringParameterWithAuth("country", "some description", 71 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 72 | {Name: "other-auth-service", Field: "user_id"}}), 73 | }, 74 | }, 75 | }, 76 | }, 77 | { 78 | desc: "with template parameters", 79 | in: ` 80 | tools: 81 | example_tool: 82 | kind: cassandra-cql 83 | source: my-cassandra-instance 84 | description: some description 85 | statement: | 86 | SELECT * FROM CQL_STATEMENT; 87 | authRequired: 88 | - my-google-auth-service 89 | - other-auth-service 90 | parameters: 91 | - name: country 92 | type: string 93 | description: some description 94 | authServices: 95 | - name: my-google-auth-service 96 | field: user_id 97 | - name: other-auth-service 98 | field: user_id 99 | templateParameters: 100 | - name: tableName 101 | type: string 102 | description: some description. 103 | - name: fieldArray 104 | type: array 105 | description: The columns to return for the query. 106 | items: 107 | name: column 108 | type: string 109 | description: A column name that will be returned from the query. 110 | `, 111 | want: server.ToolConfigs{ 112 | "example_tool": cassandracql.Config{ 113 | Name: "example_tool", 114 | Kind: "cassandra-cql", 115 | Source: "my-cassandra-instance", 116 | Description: "some description", 117 | Statement: "SELECT * FROM CQL_STATEMENT;\n", 118 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 119 | Parameters: []tools.Parameter{ 120 | tools.NewStringParameterWithAuth("country", "some description", 121 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 122 | {Name: "other-auth-service", Field: "user_id"}}), 123 | }, 124 | TemplateParameters: []tools.Parameter{ 125 | tools.NewStringParameter("tableName", "some description."), 126 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 127 | }, 128 | }, 129 | }, 130 | }, 131 | { 132 | desc: "without optional fields", 133 | in: ` 134 | tools: 135 | example_tool: 136 | kind: cassandra-cql 137 | source: my-cassandra-instance 138 | description: some description 139 | statement: | 140 | SELECT * FROM CQL_STATEMENT; 141 | `, 142 | want: server.ToolConfigs{ 143 | "example_tool": cassandracql.Config{ 144 | Name: "example_tool", 145 | Kind: "cassandra-cql", 146 | Source: "my-cassandra-instance", 147 | Description: "some description", 148 | Statement: "SELECT * FROM CQL_STATEMENT;\n", 149 | AuthRequired: []string{}, 150 | Parameters: nil, 151 | TemplateParameters: nil, 152 | }, 153 | }, 154 | }, 155 | } 156 | for _, tc := range tcs { 157 | t.Run(tc.desc, func(t *testing.T) { 158 | got := struct { 159 | Tools server.ToolConfigs `yaml:"tools"` 160 | }{} 161 | // Parse contents 162 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 163 | if err != nil { 164 | t.Fatalf("unable to unmarshal: %s", err) 165 | } 166 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 167 | t.Fatalf("incorrect parse: diff %v", diff) 168 | } 169 | }) 170 | } 171 | } 172 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/alloydbainl/alloydb-ai-nl.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "alloydb-ai-nl" 3 | type: docs 4 | weight: 1 5 | description: > 6 | The "alloydb-ai-nl" tool leverages 7 | [AlloyDB AI](https://cloud.google.com/alloydb/ai) next-generation Natural 8 | Language support to provide the ability to query the database directly using 9 | natural language. 10 | aliases: 11 | - /resources/tools/alloydb-ai-nl 12 | --- 13 | 14 | ## About 15 | 16 | The `alloydb-ai-nl` tool leverages [AlloyDB AI next-generation natural 17 | Language][alloydb-ai-nl-overview] support to allow an Agent the ability to query 18 | the database directly using natural language. Natural language streamlines the 19 | development of generative AI applications by transferring the complexity of 20 | converting natural language to SQL from the application layer to the database 21 | layer. 22 | 23 | This tool is compatible with the following sources: 24 | 25 | - [alloydb-postgres](../../sources/alloydb-pg.md) 26 | 27 | AlloyDB AI Natural Language delivers secure and accurate responses for 28 | application end user natural language questions. Natural language streamlines 29 | the development of generative AI applications by transferring the complexity 30 | of converting natural language to SQL from the application layer to the 31 | database layer. 32 | 33 | ## Requirements 34 | 35 | {{< notice tip >}} AlloyDB AI natural language is currently in gated public 36 | preview. For more information on availability and limitations, please see 37 | [AlloyDB AI natural language overview](https://cloud.google.com/alloydb/docs/ai/natural-language-overview) 38 | {{< /notice >}} 39 | 40 | To enable AlloyDB AI natural language for your AlloyDB cluster, please follow 41 | the steps listed in the [Generate SQL queries that answer natural language 42 | questions][alloydb-ai-gen-nl], including enabling the extension and configuring 43 | context for your application. 44 | 45 | [alloydb-ai-nl-overview]: https://cloud.google.com/alloydb/docs/ai/natural-language-overview 46 | [alloydb-ai-gen-nl]: https://cloud.google.com/alloydb/docs/ai/generate-sql-queries-natural-language 47 | 48 | ## Configuration 49 | 50 | ### Specifying an `nl_config` 51 | 52 | A `nl_config` is a configuration that associates an application to schema 53 | objects, examples and other contexts that can be used. A large application can 54 | also use different configurations for different parts of the app, as long as the 55 | correct configuration can be specified when a question is sent from that part of 56 | the application. 57 | 58 | Once you've followed the steps for configuring context, you can use the 59 | `context` field when configuring a `alloydb-ai-nl` tool. When this tool is 60 | invoked, the SQL will be generated and executed using this context. 61 | 62 | ### Specifying Parameters to PSV's 63 | 64 | [Parameterized Secure Views (PSVs)][alloydb-psv] are a feature unique to AlloyDB 65 | that allows you to require one or more named parameter values passed 66 | to the view when querying it, somewhat like bind variables with ordinary 67 | database queries. 68 | 69 | You can use the `nlConfigParameters` to list the parameters required for your 70 | `nl_config`. You **must** supply all parameters required for all PSVs in the 71 | context. It's strongly recommended to use features like [Authenticated 72 | Parameters](../#array-parameters) or Bound Parameters to provide secure 73 | access to queries generated using natural language, as these parameters are not 74 | visible to the LLM. 75 | 76 | [alloydb-psv]: https://cloud.google.com/alloydb/docs/parameterized-secure-views-overview 77 | 78 | {{< notice tip >}} Make sure to enable the `parameterized_views` extension before running this tool. You can do so by running this command in the AlloyDB studio: 79 | ```sql 80 | CREATE EXTENSION IF NOT EXISTS parameterized_views; 81 | ``` 82 | {{< /notice >}} 83 | 84 | ## Example 85 | 86 | ```yaml 87 | tools: 88 | ask_questions: 89 | kind: alloydb-ai-nl 90 | source: my-alloydb-source 91 | description: "Ask questions to check information about flights" 92 | nlConfig: "cymbal_air_nl_config" 93 | nlConfigParameters: 94 | - name: user_email 95 | type: string 96 | description: User ID of the logged in user. 97 | # note: we strongly recommend using features like Authenticated or 98 | # Bound parameters to prevent the LLM from seeing these params and 99 | # specifying values it shouldn't in the tool input 100 | authServices: 101 | - name: my_google_service 102 | field: email 103 | ``` 104 | ## Reference 105 | 106 | | **field** | **type** | **required** | **description** | 107 | |--------------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------| 108 | | kind | string | true | Must be "alloydb-ai-nl". | 109 | | source | string | true | Name of the AlloyDB source the natural language query should execute on. | 110 | | description | string | true | Description of the tool that is passed to the LLM. | 111 | | nlConfig | string | true | The name of the `nl_config` in AlloyDB | 112 | | nlConfigParameters | [parameters](../#specifying-parameters) | true | List of PSV parameters defined in the `nl_config` | 113 | ``` -------------------------------------------------------------------------------- /internal/sources/couchbase/couchbase.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package couchbase 16 | 17 | import ( 18 | "context" 19 | "crypto/tls" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/couchbase/gocb/v2" 24 | tlsutil "github.com/couchbase/tools-common/http/tls" 25 | "github.com/goccy/go-yaml" 26 | "github.com/googleapis/genai-toolbox/internal/sources" 27 | "go.opentelemetry.io/otel/trace" 28 | ) 29 | 30 | const SourceKind string = "couchbase" 31 | 32 | // validate interface 33 | var _ sources.SourceConfig = Config{} 34 | 35 | func init() { 36 | if !sources.Register(SourceKind, newConfig) { 37 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 38 | } 39 | } 40 | 41 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 42 | actual := Config{Name: name} 43 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 44 | return nil, err 45 | } 46 | return actual, nil 47 | } 48 | 49 | type Config struct { 50 | Name string `yaml:"name" validate:"required"` 51 | Kind string `yaml:"kind" validate:"required"` 52 | ConnectionString string `yaml:"connectionString" validate:"required"` 53 | Bucket string `yaml:"bucket" validate:"required"` 54 | Scope string `yaml:"scope" validate:"required"` 55 | Username string `yaml:"username"` 56 | Password string `yaml:"password"` 57 | ClientCert string `yaml:"clientCert"` 58 | ClientCertPassword string `yaml:"clientCertPassword"` 59 | ClientKey string `yaml:"clientKey"` 60 | ClientKeyPassword string `yaml:"clientKeyPassword"` 61 | CACert string `yaml:"caCert"` 62 | NoSSLVerify bool `yaml:"noSslVerify"` 63 | Profile string `yaml:"profile"` 64 | QueryScanConsistency uint `yaml:"queryScanConsistency"` 65 | } 66 | 67 | func (r Config) SourceConfigKind() string { 68 | return SourceKind 69 | } 70 | 71 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 72 | 73 | opts, err := r.createCouchbaseOptions() 74 | if err != nil { 75 | return nil, err 76 | } 77 | cluster, err := gocb.Connect(r.ConnectionString, opts) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | scope := cluster.Bucket(r.Bucket).Scope(r.Scope) 83 | s := &Source{ 84 | Name: r.Name, 85 | Kind: SourceKind, 86 | QueryScanConsistency: r.QueryScanConsistency, 87 | Scope: scope, 88 | } 89 | return s, nil 90 | } 91 | 92 | var _ sources.Source = &Source{} 93 | 94 | type Source struct { 95 | Name string `yaml:"name"` 96 | Kind string `yaml:"kind"` 97 | QueryScanConsistency uint `yaml:"queryScanConsistency"` 98 | Scope *gocb.Scope 99 | } 100 | 101 | func (s *Source) SourceKind() string { 102 | return SourceKind 103 | } 104 | 105 | func (s *Source) CouchbaseScope() *gocb.Scope { 106 | return s.Scope 107 | } 108 | 109 | func (s *Source) CouchbaseQueryScanConsistency() uint { 110 | return s.QueryScanConsistency 111 | } 112 | 113 | func (r Config) createCouchbaseOptions() (gocb.ClusterOptions, error) { 114 | cbOpts := gocb.ClusterOptions{} 115 | 116 | if r.Username != "" { 117 | auth := gocb.PasswordAuthenticator{ 118 | Username: r.Username, 119 | Password: r.Password, 120 | } 121 | cbOpts.Authenticator = auth 122 | } 123 | 124 | var clientCert, clientKey, caCert []byte 125 | var err error 126 | if r.ClientCert != "" { 127 | clientCert, err = os.ReadFile(r.ClientCert) 128 | if err != nil { 129 | return gocb.ClusterOptions{}, err 130 | } 131 | } 132 | 133 | if r.ClientKey != "" { 134 | clientKey, err = os.ReadFile(r.ClientKey) 135 | if err != nil { 136 | return gocb.ClusterOptions{}, err 137 | } 138 | } 139 | if r.CACert != "" { 140 | caCert, err = os.ReadFile(r.CACert) 141 | if err != nil { 142 | return gocb.ClusterOptions{}, err 143 | } 144 | } 145 | if clientCert != nil || caCert != nil { 146 | // tls parsing code is similar to the code used in the cbimport. 147 | tlsConfig, err := tlsutil.NewConfig(tlsutil.ConfigOptions{ 148 | ClientCert: clientCert, 149 | ClientKey: clientKey, 150 | Password: []byte(getCertKeyPassword(r.ClientCertPassword, r.ClientKeyPassword)), 151 | ClientAuthType: tls.VerifyClientCertIfGiven, 152 | RootCAs: caCert, 153 | NoSSLVerify: r.NoSSLVerify, 154 | }) 155 | if err != nil { 156 | return gocb.ClusterOptions{}, err 157 | } 158 | 159 | if r.ClientCert != "" { 160 | auth := gocb.CertificateAuthenticator{ 161 | ClientCertificate: &tlsConfig.Certificates[0], 162 | } 163 | cbOpts.Authenticator = auth 164 | } 165 | if r.CACert != "" { 166 | cbOpts.SecurityConfig = gocb.SecurityConfig{ 167 | TLSSkipVerify: r.NoSSLVerify, 168 | TLSRootCAs: tlsConfig.RootCAs, 169 | } 170 | } 171 | if r.NoSSLVerify { 172 | cbOpts.SecurityConfig = gocb.SecurityConfig{ 173 | TLSSkipVerify: r.NoSSLVerify, 174 | } 175 | } 176 | } 177 | if r.Profile != "" { 178 | err = cbOpts.ApplyProfile(gocb.ClusterConfigProfile(r.Profile)) 179 | if err != nil { 180 | return gocb.ClusterOptions{}, err 181 | } 182 | } 183 | return cbOpts, nil 184 | } 185 | 186 | // GetCertKeyPassword - Returns the password which should be used when creating a new TLS config. 187 | func getCertKeyPassword(certPassword, keyPassword string) string { 188 | if keyPassword != "" { 189 | return keyPassword 190 | } 191 | 192 | return certPassword 193 | } 194 | ``` -------------------------------------------------------------------------------- /internal/tools/firestore/util/validator_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 util 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | ) 21 | 22 | func TestValidateCollectionPath(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | path string 26 | wantErr bool 27 | errMsg string 28 | }{ 29 | // Valid cases 30 | { 31 | name: "valid root collection", 32 | path: "users", 33 | wantErr: false, 34 | }, 35 | { 36 | name: "valid subcollection", 37 | path: "users/user123/posts", 38 | wantErr: false, 39 | }, 40 | { 41 | name: "valid deeply nested", 42 | path: "users/user123/posts/post456/comments", 43 | wantErr: false, 44 | }, 45 | 46 | // Invalid cases 47 | { 48 | name: "empty path", 49 | path: "", 50 | wantErr: true, 51 | errMsg: "collection path cannot be empty", 52 | }, 53 | { 54 | name: "even segments (document path)", 55 | path: "users/user123", 56 | wantErr: true, 57 | errMsg: "must have an odd number of segments", 58 | }, 59 | { 60 | name: "absolute path", 61 | path: "projects/my-project/databases/(default)/documents/users", 62 | wantErr: true, 63 | errMsg: "path must be relative", 64 | }, 65 | { 66 | name: "reserved prefix __", 67 | path: "__users", 68 | wantErr: true, 69 | errMsg: "collection ID cannot start with '__'", 70 | }, 71 | { 72 | name: "dot segment", 73 | path: "users/./posts", 74 | wantErr: true, 75 | errMsg: "segment cannot be '.'", 76 | }, 77 | { 78 | name: "double slashes", 79 | path: "users//posts", 80 | wantErr: true, 81 | errMsg: "segment cannot be empty", 82 | }, 83 | { 84 | name: "trailing slash", 85 | path: "users/", 86 | wantErr: true, 87 | errMsg: "must have an odd number of segments", 88 | }, 89 | { 90 | name: "whitespace only segment", 91 | path: "users/ /posts", 92 | wantErr: true, 93 | errMsg: "segment cannot be only whitespace", 94 | }, 95 | { 96 | name: "tab whitespace segment", 97 | path: "users/\t/posts", 98 | wantErr: true, 99 | errMsg: "segment cannot be only whitespace", 100 | }, 101 | } 102 | 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | err := ValidateCollectionPath(tt.path) 106 | if tt.wantErr { 107 | if err == nil { 108 | t.Errorf("ValidateCollectionPath(%q) expected error but got none", tt.path) 109 | } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { 110 | t.Errorf("ValidateCollectionPath(%q) error = %v, want error containing %q", tt.path, err, tt.errMsg) 111 | } 112 | } else { 113 | if err != nil { 114 | t.Errorf("ValidateCollectionPath(%q) unexpected error: %v", tt.path, err) 115 | } 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func TestValidateDocumentPath(t *testing.T) { 122 | tests := []struct { 123 | name string 124 | path string 125 | wantErr bool 126 | errMsg string 127 | }{ 128 | // Valid cases 129 | { 130 | name: "valid root document", 131 | path: "users/user123", 132 | wantErr: false, 133 | }, 134 | { 135 | name: "valid nested document", 136 | path: "users/user123/posts/post456", 137 | wantErr: false, 138 | }, 139 | { 140 | name: "valid deeply nested", 141 | path: "users/user123/posts/post456/comments/comment789", 142 | wantErr: false, 143 | }, 144 | 145 | // Invalid cases 146 | { 147 | name: "empty path", 148 | path: "", 149 | wantErr: true, 150 | errMsg: "document path cannot be empty", 151 | }, 152 | { 153 | name: "odd segments (collection path)", 154 | path: "users", 155 | wantErr: true, 156 | errMsg: "must have an even number of segments", 157 | }, 158 | { 159 | name: "absolute path", 160 | path: "projects/my-project/databases/(default)/documents/users/user123", 161 | wantErr: true, 162 | errMsg: "path must be relative", 163 | }, 164 | { 165 | name: "reserved prefix __", 166 | path: "users/__user123", 167 | wantErr: true, 168 | errMsg: "document ID cannot start with '__'", 169 | }, 170 | { 171 | name: "double dot segment", 172 | path: "users/..", 173 | wantErr: true, 174 | errMsg: "segment cannot be '.'", 175 | }, 176 | { 177 | name: "double slashes in document path", 178 | path: "users//user123", 179 | wantErr: true, 180 | errMsg: "must have an even number of segments", 181 | }, 182 | { 183 | name: "trailing slash document", 184 | path: "users/user123/", 185 | wantErr: true, 186 | errMsg: "must have an even number of segments", 187 | }, 188 | { 189 | name: "whitespace only document ID", 190 | path: "users/ ", 191 | wantErr: true, 192 | errMsg: "segment cannot be only whitespace", 193 | }, 194 | { 195 | name: "whitespace in middle segment", 196 | path: "users/user123/posts/ \t ", 197 | wantErr: true, 198 | errMsg: "segment cannot be only whitespace", 199 | }, 200 | } 201 | 202 | for _, tt := range tests { 203 | t.Run(tt.name, func(t *testing.T) { 204 | err := ValidateDocumentPath(tt.path) 205 | if tt.wantErr { 206 | if err == nil { 207 | t.Errorf("ValidateDocumentPath(%q) expected error but got none", tt.path) 208 | } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { 209 | t.Errorf("ValidateDocumentPath(%q) error = %v, want error containing %q", tt.path, err, tt.errMsg) 210 | } 211 | } else { 212 | if err != nil { 213 | t.Errorf("ValidateDocumentPath(%q) unexpected error: %v", tt.path, err) 214 | } 215 | } 216 | }) 217 | } 218 | } 219 | ``` -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "log/slog" 22 | "strings" 23 | ) 24 | 25 | // StdLogger is the standard logger 26 | type StdLogger struct { 27 | outLogger *slog.Logger 28 | errLogger *slog.Logger 29 | } 30 | 31 | // NewStdLogger create a Logger that uses out and err for informational and error messages. 32 | func NewStdLogger(outW, errW io.Writer, logLevel string) (Logger, error) { 33 | //Set log level 34 | var programLevel = new(slog.LevelVar) 35 | slogLevel, err := SeverityToLevel(logLevel) 36 | if err != nil { 37 | return nil, err 38 | } 39 | programLevel.Set(slogLevel) 40 | 41 | handlerOptions := &slog.HandlerOptions{Level: programLevel} 42 | 43 | return &StdLogger{ 44 | outLogger: slog.New(NewValueTextHandler(outW, handlerOptions)), 45 | errLogger: slog.New(NewValueTextHandler(errW, handlerOptions)), 46 | }, nil 47 | } 48 | 49 | // DebugContext logs debug messages 50 | func (sl *StdLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 51 | sl.outLogger.DebugContext(ctx, msg, keysAndValues...) 52 | } 53 | 54 | // InfoContext logs debug messages 55 | func (sl *StdLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 56 | sl.outLogger.InfoContext(ctx, msg, keysAndValues...) 57 | } 58 | 59 | // WarnContext logs warning messages 60 | func (sl *StdLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 61 | sl.errLogger.WarnContext(ctx, msg, keysAndValues...) 62 | } 63 | 64 | // ErrorContext logs error messages 65 | func (sl *StdLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 66 | sl.errLogger.ErrorContext(ctx, msg, keysAndValues...) 67 | } 68 | 69 | const ( 70 | Debug = "DEBUG" 71 | Info = "INFO" 72 | Warn = "WARN" 73 | Error = "ERROR" 74 | ) 75 | 76 | // Returns severity level based on string. 77 | func SeverityToLevel(s string) (slog.Level, error) { 78 | switch strings.ToUpper(s) { 79 | case Debug: 80 | return slog.LevelDebug, nil 81 | case Info: 82 | return slog.LevelInfo, nil 83 | case Warn: 84 | return slog.LevelWarn, nil 85 | case Error: 86 | return slog.LevelError, nil 87 | default: 88 | return slog.Level(-5), fmt.Errorf("invalid log level") 89 | } 90 | } 91 | 92 | // Returns severity string based on level. 93 | func levelToSeverity(s string) (string, error) { 94 | switch s { 95 | case slog.LevelDebug.String(): 96 | return Debug, nil 97 | case slog.LevelInfo.String(): 98 | return Info, nil 99 | case slog.LevelWarn.String(): 100 | return Warn, nil 101 | case slog.LevelError.String(): 102 | return Error, nil 103 | default: 104 | return "", fmt.Errorf("invalid slog level") 105 | } 106 | } 107 | 108 | type StructuredLogger struct { 109 | outLogger *slog.Logger 110 | errLogger *slog.Logger 111 | } 112 | 113 | // NewStructuredLogger create a Logger that logs messages using JSON. 114 | func NewStructuredLogger(outW, errW io.Writer, logLevel string) (Logger, error) { 115 | //Set log level 116 | var programLevel = new(slog.LevelVar) 117 | slogLevel, err := SeverityToLevel(logLevel) 118 | if err != nil { 119 | return nil, err 120 | } 121 | programLevel.Set(slogLevel) 122 | 123 | replace := func(groups []string, a slog.Attr) slog.Attr { 124 | switch a.Key { 125 | case slog.LevelKey: 126 | value := a.Value.String() 127 | sev, _ := levelToSeverity(value) 128 | return slog.Attr{ 129 | Key: "severity", 130 | Value: slog.StringValue(sev), 131 | } 132 | case slog.MessageKey: 133 | return slog.Attr{ 134 | Key: "message", 135 | Value: a.Value, 136 | } 137 | case slog.SourceKey: 138 | return slog.Attr{ 139 | Key: "logging.googleapis.com/sourceLocation", 140 | Value: a.Value, 141 | } 142 | case slog.TimeKey: 143 | return slog.Attr{ 144 | Key: "timestamp", 145 | Value: a.Value, 146 | } 147 | } 148 | return a 149 | } 150 | 151 | // Configure structured logs to adhere to Cloud LogEntry format 152 | // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry 153 | outHandler := handlerWithSpanContext(slog.NewJSONHandler(outW, &slog.HandlerOptions{ 154 | AddSource: true, 155 | Level: programLevel, 156 | ReplaceAttr: replace, 157 | })) 158 | errHandler := handlerWithSpanContext(slog.NewJSONHandler(errW, &slog.HandlerOptions{ 159 | AddSource: true, 160 | Level: programLevel, 161 | ReplaceAttr: replace, 162 | })) 163 | 164 | return &StructuredLogger{outLogger: slog.New(outHandler), errLogger: slog.New(errHandler)}, nil 165 | } 166 | 167 | // DebugContext logs debug messages 168 | func (sl *StructuredLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 169 | sl.outLogger.DebugContext(ctx, msg, keysAndValues...) 170 | } 171 | 172 | // InfoContext logs info messages 173 | func (sl *StructuredLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 174 | sl.outLogger.InfoContext(ctx, msg, keysAndValues...) 175 | } 176 | 177 | // WarnContext logs warning messages 178 | func (sl *StructuredLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 179 | sl.errLogger.WarnContext(ctx, msg, keysAndValues...) 180 | } 181 | 182 | // ErrorContext logs error messages 183 | func (sl *StructuredLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...interface{}) { 184 | sl.errLogger.ErrorContext(ctx, msg, keysAndValues...) 185 | } 186 | ``` -------------------------------------------------------------------------------- /internal/tools/yugabytedbsql/yugabytedbsql.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 yugabytedbsql 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | "github.com/googleapis/genai-toolbox/internal/sources/yugabytedb" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/yugabyte/pgx/v5/pgxpool" 26 | ) 27 | 28 | const kind string = "yugabytedb-sql" 29 | 30 | func init() { 31 | if !tools.Register(kind, newConfig) { 32 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 33 | } 34 | } 35 | 36 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 37 | actual := Config{Name: name} 38 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 39 | return nil, err 40 | } 41 | return actual, nil 42 | } 43 | 44 | type compatibleSource interface { 45 | YugabyteDBPool() *pgxpool.Pool 46 | } 47 | 48 | var compatibleSources = [...]string{yugabytedb.SourceKind} 49 | 50 | type Config struct { 51 | Name string `yaml:"name" validate:"required"` 52 | Kind string `yaml:"kind" validate:"required"` 53 | Source string `yaml:"source" validate:"required"` 54 | Description string `yaml:"description" validate:"required"` 55 | Statement string `yaml:"statement" validate:"required"` 56 | AuthRequired []string `yaml:"authRequired"` 57 | Parameters tools.Parameters `yaml:"parameters"` 58 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 59 | } 60 | 61 | // validate interface 62 | var _ tools.ToolConfig = Config{} 63 | 64 | func (cfg Config) ToolConfigKind() string { 65 | return kind 66 | } 67 | 68 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 69 | // verify source exists 70 | rawS, ok := srcs[cfg.Source] 71 | if !ok { 72 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 73 | } 74 | 75 | // verify the source is compatible 76 | s, ok := rawS.(compatibleSource) 77 | if !ok { 78 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 79 | } 80 | 81 | allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 87 | 88 | // finish tool setup 89 | t := Tool{ 90 | Name: cfg.Name, 91 | Kind: kind, 92 | Parameters: cfg.Parameters, 93 | TemplateParameters: cfg.TemplateParameters, 94 | AllParams: allParameters, 95 | Statement: cfg.Statement, 96 | AuthRequired: cfg.AuthRequired, 97 | Pool: s.YugabyteDBPool(), 98 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 99 | mcpManifest: mcpManifest, 100 | } 101 | return t, nil 102 | } 103 | 104 | // validate interface 105 | var _ tools.Tool = Tool{} 106 | 107 | type Tool struct { 108 | Name string `yaml:"name"` 109 | Kind string `yaml:"kind"` 110 | AuthRequired []string `yaml:"authRequired"` 111 | Parameters tools.Parameters `yaml:"parameters"` 112 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 113 | AllParams tools.Parameters `yaml:"allParams"` 114 | 115 | Pool *pgxpool.Pool 116 | Statement string 117 | manifest tools.Manifest 118 | mcpManifest tools.McpManifest 119 | } 120 | 121 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 122 | paramsMap := params.AsMap() 123 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 124 | if err != nil { 125 | return nil, fmt.Errorf("unable to extract template params %w", err) 126 | } 127 | 128 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 129 | if err != nil { 130 | return nil, fmt.Errorf("unable to extract standard params %w", err) 131 | } 132 | sliceParams := newParams.AsSlice() 133 | results, err := t.Pool.Query(ctx, newStatement, sliceParams...) 134 | if err != nil { 135 | return nil, fmt.Errorf("unable to execute query: %w", err) 136 | } 137 | 138 | fields := results.FieldDescriptions() 139 | 140 | var out []any 141 | for results.Next() { 142 | v, err := results.Values() 143 | if err != nil { 144 | return nil, fmt.Errorf("unable to parse row: %w", err) 145 | } 146 | vMap := make(map[string]any) 147 | for i, f := range fields { 148 | vMap[f.Name] = v[i] 149 | } 150 | out = append(out, vMap) 151 | } 152 | 153 | return out, nil 154 | } 155 | 156 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 157 | return tools.ParseParams(t.AllParams, data, claims) 158 | } 159 | 160 | func (t Tool) Manifest() tools.Manifest { 161 | return t.manifest 162 | } 163 | 164 | func (t Tool) McpManifest() tools.McpManifest { 165 | return t.mcpManifest 166 | } 167 | 168 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 169 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 170 | } 171 | 172 | func (t Tool) RequiresClientAuthorization() bool { 173 | return false 174 | } 175 | ``` -------------------------------------------------------------------------------- /docs/en/resources/sources/cloud-sql-mssql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Cloud SQL for SQL Server" 3 | linkTitle: "Cloud SQL (SQL Server)" 4 | type: docs 5 | weight: 1 6 | description: > 7 | Cloud SQL for SQL Server is a fully-managed database service for SQL Server. 8 | --- 9 | 10 | ## About 11 | 12 | [Cloud SQL for SQL Server][csql-mssql-docs] is a managed database service that 13 | helps you set up, maintain, manage, and administer your SQL Server databases on 14 | Google Cloud. 15 | 16 | If you are new to Cloud SQL for SQL Server, you can try [creating and connecting 17 | to a database by following these instructions][csql-mssql-connect]. 18 | 19 | [csql-mssql-docs]: https://cloud.google.com/sql/docs/sqlserver 20 | [csql-mssql-connect]: https://cloud.google.com/sql/docs/sqlserver/connect-overview 21 | 22 | ## Available Tools 23 | 24 | - [`mssql-sql`](../tools/mssql/mssql-sql.md) 25 | Execute pre-defined SQL Server queries with placeholder parameters. 26 | 27 | - [`mssql-execute-sql`](../tools/mssql/mssql-execute-sql.md) 28 | Run parameterized SQL Server queries in Cloud SQL for SQL Server. 29 | 30 | - [`mssql-list-tables`](../tools/mssql/mssql-list-tables.md) 31 | List tables in a Cloud SQL for SQL Server database. 32 | 33 | ### Pre-built Configurations 34 | 35 | - [Cloud SQL for SQL Server using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/cloud_sql_mssql_mcp/) 36 | Connect your IDE to Cloud SQL for SQL Server using Toolbox. 37 | 38 | ## Requirements 39 | 40 | ### IAM Permissions 41 | 42 | By default, this source uses the [Cloud SQL Go Connector][csql-go-conn] to 43 | authorize and establish mTLS connections to your Cloud SQL instance. The Go 44 | connector uses your [Application Default Credentials (ADC)][adc] to authorize 45 | your connection to Cloud SQL. 46 | 47 | In addition to [setting the ADC for your server][set-adc], you need to ensure 48 | the IAM identity has been given the following IAM roles (or corresponding 49 | permissions): 50 | 51 | - `roles/cloudsql.client` 52 | 53 | {{< notice tip >}} 54 | If you are connecting from Compute Engine, make sure your VM 55 | also has the [proper 56 | scope](https://cloud.google.com/compute/docs/access/service-accounts#accesscopesiam) 57 | to connect using the Cloud SQL Admin API. 58 | {{< /notice >}} 59 | 60 | [csql-go-conn]: https://github.com/GoogleCloudPlatform/cloud-sql-go-connector 61 | [adc]: https://cloud.google.com/docs/authentication#adc 62 | [set-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc 63 | 64 | ### Networking 65 | 66 | Cloud SQL supports connecting over both from external networks via the internet 67 | ([public IP][public-ip]), and internal networks ([private IP][private-ip]). 68 | For more information on choosing between the two options, see the Cloud SQL page 69 | [Connection overview][conn-overview]. 70 | 71 | You can configure the `ipType` parameter in your source configuration to 72 | `public` or `private` to match your cluster's configuration. Regardless of which 73 | you choose, all connections use IAM-based authorization and are encrypted with 74 | mTLS. 75 | 76 | [private-ip]: https://cloud.google.com/sql/docs/sqlserver/configure-private-ip 77 | [public-ip]: https://cloud.google.com/sql/docs/sqlserver/configure-ip 78 | [conn-overview]: https://cloud.google.com/sql/docs/sqlserver/connect-overview 79 | 80 | ### Database User 81 | 82 | Currently, this source only uses standard authentication. You will need to 83 | [create a SQL Server user][cloud-sql-users] to login to the database with. 84 | 85 | [cloud-sql-users]: https://cloud.google.com/sql/docs/sqlserver/create-manage-users 86 | 87 | ## Example 88 | 89 | ```yaml 90 | sources: 91 | my-cloud-sql-mssql-instance: 92 | kind: cloud-sql-mssql 93 | project: my-project 94 | region: my-region 95 | instance: my-instance 96 | database: my_db 97 | ipAddress: localhost 98 | user: ${USER_NAME} 99 | password: ${PASSWORD} 100 | # ipType: private 101 | ``` 102 | 103 | {{< notice tip >}} 104 | Use environment variable replacement with the format ${ENV_NAME} 105 | instead of hardcoding your secrets into the configuration file. 106 | {{< /notice >}} 107 | 108 | ## Reference 109 | 110 | | **field** | **type** | **required** | **description** | 111 | |-----------|:--------:|:------------:|------------------------------------------------------------------------------------------------------| 112 | | kind | string | true | Must be "cloud-sql-mssql". | 113 | | project | string | true | Id of the GCP project that the cluster was created in (e.g. "my-project-id"). | 114 | | region | string | true | Name of the GCP region that the cluster was created in (e.g. "us-central1"). | 115 | | instance | string | true | Name of the Cloud SQL instance within the cluster (e.g. "my-instance"). | 116 | | database | string | true | Name of the Cloud SQL database to connect to (e.g. "my_db"). | 117 | | ipAddress | string | true | IP address of the Cloud SQL instance to connect to. | 118 | | user | string | true | Name of the SQL Server user to connect as (e.g. "my-pg-user"). | 119 | | password | string | true | Password of the SQL Server user (e.g. "my-password"). | 120 | | ipType | string | false | IP Type of the Cloud SQL instance, must be either `public`, `private`, or `psc`. Default: `public`. | 121 | ``` -------------------------------------------------------------------------------- /internal/sources/mysql/mysql_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 mysql_test 16 | 17 | import ( 18 | "context" 19 | "strings" 20 | "testing" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/google/go-cmp/cmp/cmpopts" 25 | "go.opentelemetry.io/otel/trace/noop" 26 | 27 | "github.com/googleapis/genai-toolbox/internal/server" 28 | "github.com/googleapis/genai-toolbox/internal/sources/mysql" 29 | "github.com/googleapis/genai-toolbox/internal/testutils" 30 | ) 31 | 32 | func TestParseFromYamlCloudSQLMySQL(t *testing.T) { 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.SourceConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | sources: 42 | my-mysql-instance: 43 | kind: mysql 44 | host: 0.0.0.0 45 | port: my-port 46 | database: my_db 47 | user: my_user 48 | password: my_pass 49 | `, 50 | want: server.SourceConfigs{ 51 | "my-mysql-instance": mysql.Config{ 52 | Name: "my-mysql-instance", 53 | Kind: mysql.SourceKind, 54 | Host: "0.0.0.0", 55 | Port: "my-port", 56 | Database: "my_db", 57 | User: "my_user", 58 | Password: "my_pass", 59 | }, 60 | }, 61 | }, 62 | { 63 | desc: "with query timeout", 64 | in: ` 65 | sources: 66 | my-mysql-instance: 67 | kind: mysql 68 | host: 0.0.0.0 69 | port: my-port 70 | database: my_db 71 | user: my_user 72 | password: my_pass 73 | queryTimeout: 45s 74 | `, 75 | want: server.SourceConfigs{ 76 | "my-mysql-instance": mysql.Config{ 77 | Name: "my-mysql-instance", 78 | Kind: mysql.SourceKind, 79 | Host: "0.0.0.0", 80 | Port: "my-port", 81 | Database: "my_db", 82 | User: "my_user", 83 | Password: "my_pass", 84 | QueryTimeout: "45s", 85 | }, 86 | }, 87 | }, 88 | { 89 | desc: "with query params", 90 | in: ` 91 | sources: 92 | my-mysql-instance: 93 | kind: mysql 94 | host: 0.0.0.0 95 | port: my-port 96 | database: my_db 97 | user: my_user 98 | password: my_pass 99 | queryParams: 100 | tls: preferred 101 | charset: utf8mb4 102 | `, 103 | want: server.SourceConfigs{ 104 | "my-mysql-instance": mysql.Config{ 105 | Name: "my-mysql-instance", 106 | Kind: mysql.SourceKind, 107 | Host: "0.0.0.0", 108 | Port: "my-port", 109 | Database: "my_db", 110 | User: "my_user", 111 | Password: "my_pass", 112 | QueryParams: map[string]string{ 113 | "tls": "preferred", 114 | "charset": "utf8mb4", 115 | }, 116 | }, 117 | }, 118 | }, 119 | } 120 | for _, tc := range tcs { 121 | t.Run(tc.desc, func(t *testing.T) { 122 | t.Parallel() 123 | got := struct { 124 | Sources server.SourceConfigs `yaml:"sources"` 125 | }{} 126 | // Parse contents 127 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 128 | if err != nil { 129 | t.Fatalf("unable to unmarshal: %s", err) 130 | } 131 | if diff := cmp.Diff(tc.want, got.Sources, cmpopts.EquateEmpty()); diff != "" { 132 | t.Fatalf("mismatch (-want +got):\n%s", diff) 133 | } 134 | }) 135 | } 136 | 137 | } 138 | 139 | func TestFailParseFromYaml(t *testing.T) { 140 | tcs := []struct { 141 | desc string 142 | in string 143 | err string 144 | }{ 145 | { 146 | desc: "extra field", 147 | in: ` 148 | sources: 149 | my-mysql-instance: 150 | kind: mysql 151 | host: 0.0.0.0 152 | port: my-port 153 | database: my_db 154 | user: my_user 155 | password: my_pass 156 | foo: bar 157 | `, 158 | err: "unknown field \"foo\"", 159 | }, 160 | { 161 | desc: "missing required field", 162 | in: ` 163 | sources: 164 | my-mysql-instance: 165 | kind: mysql 166 | port: my-port 167 | database: my_db 168 | user: my_user 169 | password: my_pass 170 | `, 171 | err: "Field validation for 'Host' failed", 172 | }, 173 | { 174 | desc: "invalid query params type", 175 | in: ` 176 | sources: 177 | my-mysql-instance: 178 | kind: mysql 179 | host: 0.0.0.0 180 | port: 3306 181 | database: my_db 182 | user: my_user 183 | password: my_pass 184 | queryParams: not-a-map 185 | `, 186 | err: "string was used where mapping is expected", 187 | }, 188 | } 189 | for _, tc := range tcs { 190 | t.Run(tc.desc, func(t *testing.T) { 191 | t.Parallel() 192 | got := struct { 193 | Sources server.SourceConfigs `yaml:"sources"` 194 | }{} 195 | // Parse contents 196 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 197 | if err == nil { 198 | t.Fatalf("expect parsing to fail") 199 | } 200 | errStr := err.Error() 201 | if !strings.Contains(errStr, tc.err) { 202 | t.Fatalf("unexpected error: got %q, want substring %q", errStr, tc.err) 203 | } 204 | }) 205 | } 206 | } 207 | 208 | // TestFailInitialization test error during initialization without attempting a DB connection. 209 | func TestFailInitialization(t *testing.T) { 210 | t.Parallel() 211 | 212 | cfg := mysql.Config{ 213 | Name: "instance", 214 | Kind: "mysql", 215 | Host: "localhost", 216 | Port: "3306", 217 | Database: "db", 218 | User: "user", 219 | Password: "pass", 220 | QueryTimeout: "abc", // invalid duration 221 | } 222 | _, err := cfg.Initialize(context.Background(), noop.NewTracerProvider().Tracer("test")) 223 | if err == nil { 224 | t.Fatalf("expected error for invalid queryTimeout, got nil") 225 | } 226 | if !strings.Contains(err.Error(), "invalid queryTimeout") { 227 | t.Fatalf("unexpected error: %v", err) 228 | } 229 | } 230 | ```