This is page 14 of 45. 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-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-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 │ │ ├── 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 │ │ │ ├── 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 │ │ │ ├── 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 │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /tests/mssql/mssql_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mssql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "net/url" 22 | "os" 23 | "regexp" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/google/uuid" 29 | "github.com/googleapis/genai-toolbox/internal/testutils" 30 | "github.com/googleapis/genai-toolbox/tests" 31 | _ "github.com/microsoft/go-mssqldb" 32 | ) 33 | 34 | var ( 35 | MSSQLSourceKind = "mssql" 36 | MSSQLToolKind = "mssql-sql" 37 | MSSQLDatabase = os.Getenv("MSSQL_DATABASE") 38 | MSSQLHost = os.Getenv("MSSQL_HOST") 39 | MSSQLPort = os.Getenv("MSSQL_PORT") 40 | MSSQLUser = os.Getenv("MSSQL_USER") 41 | MSSQLPass = os.Getenv("MSSQL_PASS") 42 | ) 43 | 44 | func getMsSQLVars(t *testing.T) map[string]any { 45 | switch "" { 46 | case MSSQLDatabase: 47 | t.Fatal("'MSSQL_DATABASE' not set") 48 | case MSSQLHost: 49 | t.Fatal("'MSSQL_HOST' not set") 50 | case MSSQLPort: 51 | t.Fatal("'MSSQL_PORT' not set") 52 | case MSSQLUser: 53 | t.Fatal("'MSSQL_USER' not set") 54 | case MSSQLPass: 55 | t.Fatal("'MSSQL_PASS' not set") 56 | } 57 | 58 | return map[string]any{ 59 | "kind": MSSQLSourceKind, 60 | "host": MSSQLHost, 61 | "port": MSSQLPort, 62 | "database": MSSQLDatabase, 63 | "user": MSSQLUser, 64 | "password": MSSQLPass, 65 | } 66 | } 67 | 68 | // Copied over from mssql.go 69 | func initMSSQLConnection(host, port, user, pass, dbname string) (*sql.DB, error) { 70 | // Create dsn 71 | query := url.Values{} 72 | query.Add("database", dbname) 73 | url := &url.URL{ 74 | Scheme: "sqlserver", 75 | User: url.UserPassword(user, pass), 76 | Host: fmt.Sprintf("%s:%s", host, port), 77 | RawQuery: query.Encode(), 78 | } 79 | 80 | // Open database connection 81 | db, err := sql.Open("sqlserver", url.String()) 82 | if err != nil { 83 | return nil, fmt.Errorf("sql.Open: %w", err) 84 | } 85 | return db, nil 86 | } 87 | 88 | func TestMSSQLToolEndpoints(t *testing.T) { 89 | sourceConfig := getMsSQLVars(t) 90 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 91 | defer cancel() 92 | 93 | var args []string 94 | 95 | pool, err := initMSSQLConnection(MSSQLHost, MSSQLPort, MSSQLUser, MSSQLPass, MSSQLDatabase) 96 | if err != nil { 97 | t.Fatalf("unable to create SQL Server connection pool: %s", err) 98 | } 99 | 100 | // cleanup test environment 101 | tests.CleanupMSSQLTables(t, ctx, pool) 102 | 103 | // create table name with UUID 104 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 105 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 106 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 107 | 108 | // set up data for param tool 109 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetMSSQLParamToolInfo(tableNameParam) 110 | teardownTable1 := tests.SetupMsSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 111 | defer teardownTable1(t) 112 | 113 | // set up data for auth tool 114 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMSSQLAuthToolInfo(tableNameAuth) 115 | teardownTable2 := tests.SetupMsSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 116 | defer teardownTable2(t) 117 | 118 | // Write config into a file and pass it to command 119 | toolsFile := tests.GetToolsConfig(sourceConfig, MSSQLToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 120 | toolsFile = tests.AddMSSQLExecuteSqlConfig(t, toolsFile) 121 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetMSSQLTmplToolStatement() 122 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MSSQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") 123 | toolsFile = tests.AddMSSQLPrebuiltToolConfig(t, toolsFile) 124 | 125 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 126 | if err != nil { 127 | t.Fatalf("command initialization returned an error: %s", err) 128 | } 129 | defer cleanup() 130 | 131 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 132 | defer cancel() 133 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 134 | if err != nil { 135 | t.Logf("toolbox command logs: \n%s", out) 136 | t.Fatalf("toolbox didn't start successfully: %s", err) 137 | } 138 | 139 | // Get configs for tests 140 | select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMSSQLWants() 141 | 142 | // Run tests 143 | tests.RunToolGetTest(t) 144 | tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest()) 145 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 146 | tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) 147 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 148 | 149 | // Run specific MSSQL tool tests 150 | tests.RunMSSQLListTablesTest(t, tableNameParam, tableNameAuth) 151 | } 152 | ``` -------------------------------------------------------------------------------- /internal/tools/alloydb/alloydbgetinstance/alloydbgetinstance.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 alloydbgetinstance 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-get-instance" 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 get-instance 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."), 75 | tools.NewStringParameter("location", "The location of the instance (e.g., 'us-central1')."), 76 | tools.NewStringParameter("cluster", "The ID of the cluster."), 77 | tools.NewStringParameter("instance", "The ID of the instance."), 78 | } 79 | paramManifest := allParameters.Manifest() 80 | 81 | description := cfg.Description 82 | if description == "" { 83 | description = "Retrieves details about a specific AlloyDB instance." 84 | } 85 | mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters) 86 | 87 | return Tool{ 88 | Name: cfg.Name, 89 | Kind: kind, 90 | Source: s, 91 | AllParams: allParameters, 92 | manifest: tools.Manifest{Description: description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 93 | mcpManifest: mcpManifest, 94 | }, nil 95 | } 96 | 97 | // Tool represents the get-instance tool. 98 | type Tool struct { 99 | Name string `yaml:"name"` 100 | Kind string `yaml:"kind"` 101 | 102 | Source *alloydbadmin.Source 103 | AllParams tools.Parameters 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 | instance, ok := paramsMap["instance"].(string) 126 | if !ok { 127 | return nil, fmt.Errorf("invalid 'instance' parameter; expected a string") 128 | } 129 | 130 | service, err := t.Source.GetService(ctx, string(accessToken)) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | urlString := fmt.Sprintf("projects/%s/locations/%s/clusters/%s/instances/%s", project, location, cluster, instance) 136 | 137 | resp, err := service.Projects.Locations.Clusters.Instances.Get(urlString).Do() 138 | if err != nil { 139 | return nil, fmt.Errorf("error getting AlloyDB instance: %w", err) 140 | } 141 | 142 | return resp, nil 143 | } 144 | 145 | // ParseParams parses the parameters for the tool. 146 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 147 | return tools.ParseParams(t.AllParams, data, claims) 148 | } 149 | 150 | // Manifest returns the tool's manifest. 151 | func (t Tool) Manifest() tools.Manifest { 152 | return t.manifest 153 | } 154 | 155 | // McpManifest returns the tool's MCP manifest. 156 | func (t Tool) McpManifest() tools.McpManifest { 157 | return t.mcpManifest 158 | } 159 | 160 | // Authorized checks if the tool is authorized. 161 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 162 | return true 163 | } 164 | 165 | func (t Tool) RequiresClientAuthorization() bool { 166 | return t.Source.UseClientAuthorization() 167 | } 168 | ``` -------------------------------------------------------------------------------- /internal/tools/trino/trinoexecutesql/trinoexecutesql.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 trinoexecutesql 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/trino" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | ) 27 | 28 | const kind string = "trino-execute-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 | TrinoDB() *sql.DB 46 | } 47 | 48 | // validate compatible sources are still compatible 49 | var _ compatibleSource = &trino.Source{} 50 | 51 | var compatibleSources = [...]string{trino.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 | // 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 | sqlParameter := tools.NewStringParameter("sql", "The SQL query to execute against the Trino database.") 82 | parameters := tools.Parameters{sqlParameter} 83 | 84 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 85 | 86 | // finish tool setup 87 | t := Tool{ 88 | Name: cfg.Name, 89 | Kind: kind, 90 | Parameters: parameters, 91 | AuthRequired: cfg.AuthRequired, 92 | Db: s.TrinoDB(), 93 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 94 | mcpManifest: mcpManifest, 95 | } 96 | return t, nil 97 | } 98 | 99 | // validate interface 100 | var _ tools.Tool = Tool{} 101 | 102 | type Tool struct { 103 | Name string `yaml:"name"` 104 | Kind string `yaml:"kind"` 105 | AuthRequired []string `yaml:"authRequired"` 106 | Parameters tools.Parameters `yaml:"parameters"` 107 | 108 | Db *sql.DB 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | } 112 | 113 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 114 | sliceParams := params.AsSlice() 115 | sql, ok := sliceParams[0].(string) 116 | if !ok { 117 | return nil, fmt.Errorf("unable to cast sql parameter: %v", sliceParams[0]) 118 | } 119 | 120 | results, err := t.Db.QueryContext(ctx, sql) 121 | if err != nil { 122 | return nil, fmt.Errorf("unable to execute query: %w", err) 123 | } 124 | defer results.Close() 125 | 126 | cols, err := results.Columns() 127 | if err != nil { 128 | return nil, fmt.Errorf("unable to retrieve column names: %w", err) 129 | } 130 | 131 | // create an array of values for each column, which can be re-used to scan each row 132 | rawValues := make([]any, len(cols)) 133 | values := make([]any, len(cols)) 134 | for i := range rawValues { 135 | values[i] = &rawValues[i] 136 | } 137 | 138 | var out []any 139 | for results.Next() { 140 | err := results.Scan(values...) 141 | if err != nil { 142 | return nil, fmt.Errorf("unable to parse row: %w", err) 143 | } 144 | vMap := make(map[string]any) 145 | for i, name := range cols { 146 | val := rawValues[i] 147 | if val == nil { 148 | vMap[name] = nil 149 | continue 150 | } 151 | 152 | // Convert byte arrays to strings for text fields 153 | if b, ok := val.([]byte); ok { 154 | vMap[name] = string(b) 155 | } else { 156 | vMap[name] = val 157 | } 158 | } 159 | out = append(out, vMap) 160 | } 161 | 162 | if err := results.Err(); err != nil { 163 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 164 | } 165 | 166 | return out, nil 167 | } 168 | 169 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 170 | return tools.ParseParams(t.Parameters, data, claims) 171 | } 172 | 173 | func (t Tool) Manifest() tools.Manifest { 174 | return t.manifest 175 | } 176 | 177 | func (t Tool) McpManifest() tools.McpManifest { 178 | return t.mcpManifest 179 | } 180 | 181 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 182 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 183 | } 184 | 185 | func (t Tool) RequiresClientAuthorization() bool { 186 | return false 187 | } 188 | ``` -------------------------------------------------------------------------------- /internal/tools/dataplex/dataplexsearchentries/dataplexsearchentries.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 dataplexsearchentries 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | dataplexapi "cloud.google.com/go/dataplex/apiv1" 22 | dataplexpb "cloud.google.com/go/dataplex/apiv1/dataplexpb" 23 | "github.com/goccy/go-yaml" 24 | "github.com/googleapis/genai-toolbox/internal/sources" 25 | dataplexds "github.com/googleapis/genai-toolbox/internal/sources/dataplex" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | ) 28 | 29 | const kind string = "dataplex-search-entries" 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 | CatalogClient() *dataplexapi.CatalogClient 47 | ProjectID() string 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &dataplexds.Source{} 52 | 53 | var compatibleSources = [...]string{dataplexds.SourceKind} 54 | 55 | type Config struct { 56 | Name string `yaml:"name" validate:"required"` 57 | Kind string `yaml:"kind" validate:"required"` 58 | Source string `yaml:"source" validate:"required"` 59 | Description string `yaml:"description"` 60 | AuthRequired []string `yaml:"authRequired"` 61 | } 62 | 63 | // validate interface 64 | var _ tools.ToolConfig = Config{} 65 | 66 | func (cfg Config) ToolConfigKind() string { 67 | return kind 68 | } 69 | 70 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 71 | // Initialize the search configuration with the provided sources 72 | rawS, ok := srcs[cfg.Source] 73 | if !ok { 74 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 75 | } 76 | // verify the source is compatible 77 | s, ok := rawS.(compatibleSource) 78 | if !ok { 79 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 80 | } 81 | 82 | query := tools.NewStringParameter("query", "The query against which entries in scope should be matched.") 83 | pageSize := tools.NewIntParameterWithDefault("pageSize", 5, "Number of results in the search page.") 84 | orderBy := tools.NewStringParameterWithDefault("orderBy", "relevance", "Specifies the ordering of results. Supported values are: relevance, last_modified_timestamp, last_modified_timestamp asc") 85 | parameters := tools.Parameters{query, pageSize, orderBy} 86 | 87 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 88 | 89 | t := Tool{ 90 | Name: cfg.Name, 91 | Kind: kind, 92 | Parameters: parameters, 93 | AuthRequired: cfg.AuthRequired, 94 | CatalogClient: s.CatalogClient(), 95 | ProjectID: s.ProjectID(), 96 | manifest: tools.Manifest{ 97 | Description: cfg.Description, 98 | Parameters: parameters.Manifest(), 99 | AuthRequired: cfg.AuthRequired, 100 | }, 101 | mcpManifest: mcpManifest, 102 | } 103 | return t, nil 104 | } 105 | 106 | type Tool struct { 107 | Name string 108 | Kind string 109 | Parameters tools.Parameters 110 | AuthRequired []string 111 | CatalogClient *dataplexapi.CatalogClient 112 | ProjectID string 113 | manifest tools.Manifest 114 | mcpManifest tools.McpManifest 115 | } 116 | 117 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 118 | paramsMap := params.AsMap() 119 | query, _ := paramsMap["query"].(string) 120 | pageSize := int32(paramsMap["pageSize"].(int)) 121 | orderBy, _ := paramsMap["orderBy"].(string) 122 | 123 | req := &dataplexpb.SearchEntriesRequest{ 124 | Query: query, 125 | Name: fmt.Sprintf("projects/%s/locations/global", t.ProjectID), 126 | PageSize: pageSize, 127 | OrderBy: orderBy, 128 | SemanticSearch: true, 129 | } 130 | 131 | it := t.CatalogClient.SearchEntries(ctx, req) 132 | if it == nil { 133 | return nil, fmt.Errorf("failed to create search entries iterator for project %q", t.ProjectID) 134 | } 135 | 136 | var results []*dataplexpb.SearchEntriesResult 137 | for { 138 | entry, err := it.Next() 139 | if err != nil { 140 | break 141 | } 142 | results = append(results, entry) 143 | } 144 | return results, nil 145 | } 146 | 147 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 148 | // Parse parameters from the provided data 149 | return tools.ParseParams(t.Parameters, data, claims) 150 | } 151 | 152 | func (t Tool) Manifest() tools.Manifest { 153 | // Returns the tool manifest 154 | return t.manifest 155 | } 156 | 157 | func (t Tool) McpManifest() tools.McpManifest { 158 | // Returns the tool MCP manifest 159 | return t.mcpManifest 160 | } 161 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 162 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 163 | } 164 | 165 | func (t Tool) RequiresClientAuthorization() bool { 166 | return false 167 | } 168 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/oceanbase/oceanbase-sql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "oceanbase-sql" 3 | type: docs 4 | weight: 1 5 | description: > 6 | An "oceanbase-sql" tool executes a pre-defined SQL statement against an OceanBase database. 7 | aliases: 8 | - /resources/tools/oceanbase-sql 9 | --- 10 | 11 | ## About 12 | 13 | An `oceanbase-sql` tool executes a pre-defined SQL statement against an 14 | OceanBase database. It's compatible with the following source: 15 | 16 | - [oceanbase](../sources/oceanbase.md) 17 | 18 | The specified SQL statement is executed as a [prepared 19 | statement][mysql-prepare], and expects parameters in the SQL query to be in the 20 | form of placeholders `?`. 21 | 22 | [mysql-prepare]: https://dev.mysql.com/doc/refman/8.4/en/sql-prepared-statements.html 23 | 24 | ## Example 25 | 26 | > **Note:** This tool uses parameterized queries to prevent SQL injections. 27 | > Query parameters can be used as substitutes for arbitrary expressions. 28 | > Parameters cannot be used as substitutes for identifiers, column names, table 29 | > names, or other parts of the query. 30 | 31 | ```yaml 32 | tools: 33 | search_flights_by_number: 34 | kind: oceanbase-sql 35 | source: my-oceanbase-instance 36 | statement: | 37 | SELECT * FROM flights 38 | WHERE airline = ? 39 | AND flight_number = ? 40 | LIMIT 10 41 | description: | 42 | Use this tool to get information for a specific flight. 43 | Takes an airline code and flight number and returns info on the flight. 44 | Do NOT use this tool with a flight id. Do NOT guess an airline code or flight number. 45 | Example: 46 | {{ 47 | "airline": "CY", 48 | "flight_number": "888", 49 | }} 50 | parameters: 51 | - name: airline 52 | type: string 53 | description: Airline unique 2 letter identifier 54 | - name: flight_number 55 | type: string 56 | description: 1 to 4 digit number 57 | ``` 58 | 59 | ### Example with Template Parameters 60 | 61 | > **Note:** This tool allows direct modifications to the SQL statement, 62 | > including identifiers, column names, and table names. **This makes it more 63 | > vulnerable to SQL injections**. Using basic parameters only (see above) is 64 | > recommended for performance and safety reasons. 65 | 66 | ```yaml 67 | tools: 68 | list_table: 69 | kind: oceanbase-sql 70 | source: my-oceanbase-instance 71 | statement: | 72 | SELECT * FROM {{.tableName}}; 73 | description: | 74 | Use this tool to list all information from a specific table. 75 | Example: 76 | {{ 77 | "tableName": "flights", 78 | }} 79 | templateParameters: 80 | - name: tableName 81 | type: string 82 | description: Table to select from 83 | ``` 84 | 85 | ### Example with Array Parameters 86 | 87 | ```yaml 88 | tools: 89 | search_flights_by_ids: 90 | kind: oceanbase-sql 91 | source: my-oceanbase-instance 92 | statement: | 93 | SELECT * FROM flights 94 | WHERE id IN (?) 95 | AND status IN (?) 96 | description: | 97 | Use this tool to get information for multiple flights by their IDs and statuses. 98 | Example: 99 | {{ 100 | "flight_ids": [1, 2, 3], 101 | "statuses": ["active", "scheduled"] 102 | }} 103 | parameters: 104 | - name: flight_ids 105 | type: array 106 | description: List of flight IDs to search for 107 | items: 108 | name: flight_id 109 | type: integer 110 | description: Individual flight ID 111 | - name: statuses 112 | type: array 113 | description: List of flight statuses to filter by 114 | items: 115 | name: status 116 | type: string 117 | description: Individual flight status 118 | ``` 119 | 120 | ## Reference 121 | 122 | | **field** | **type** | **required** | **description** | 123 | |--------------------|:------------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------| 124 | | kind | string | true | Must be "oceanbase-sql". | 125 | | source | string | true | Name of the source the SQL should execute on. | 126 | | description | string | true | Description of the tool that is passed to the LLM. | 127 | | statement | string | true | SQL statement to execute on. | 128 | | parameters | [parameters](_index#specifying-parameters) | false | List of [parameters](_index#specifying-parameters) that will be inserted into the SQL statement. | 129 | | templateParameters | [templateParameters](_index#template-parameters) | false | List of [templateParameters](_index#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | ``` -------------------------------------------------------------------------------- /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 | ```