This is page 18 of 47. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-create-project-file.md │ │ │ ├── looker-delete-project-file.md │ │ │ ├── looker-dev-mode.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-get-project-file.md │ │ │ ├── looker-get-project-files.md │ │ │ ├── looker-get-projects.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ ├── looker-run-look.md │ │ │ └── looker-update-project-file.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookercreateprojectfile │ │ │ │ ├── lookercreateprojectfile_test.go │ │ │ │ └── lookercreateprojectfile.go │ │ │ ├── lookerdeleteprojectfile │ │ │ │ ├── lookerdeleteprojectfile_test.go │ │ │ │ └── lookerdeleteprojectfile.go │ │ │ ├── lookerdevmode │ │ │ │ ├── lookerdevmode_test.go │ │ │ │ └── lookerdevmode.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookergetprojectfile │ │ │ │ ├── lookergetprojectfile_test.go │ │ │ │ └── lookergetprojectfile.go │ │ │ ├── lookergetprojectfiles │ │ │ │ ├── lookergetprojectfiles_test.go │ │ │ │ └── lookergetprojectfiles.go │ │ │ ├── lookergetprojects │ │ │ │ ├── lookergetprojects_test.go │ │ │ │ └── lookergetprojects.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ ├── lookerrunlook │ │ │ │ ├── lookerrunlook_test.go │ │ │ │ └── lookerrunlook.go │ │ │ └── lookerupdateprojectfile │ │ │ ├── lookerupdateprojectfile_test.go │ │ │ └── lookerupdateprojectfile.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /internal/tools/looker/lookergetlooks/lookergetlooks.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 lookergetlooks 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-looks" 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 | titleParameter := tools.NewStringParameterWithDefault("title", "", "The title of the look.") 76 | descParameter := tools.NewStringParameterWithDefault("desc", "", "The description of the look.") 77 | limitParameter := tools.NewIntParameterWithDefault("limit", 100, "The number of looks to fetch. Default 100") 78 | offsetParameter := tools.NewIntParameterWithDefault("offset", 0, "The number of looks to skip before fetching. Default 0") 79 | parameters := tools.Parameters{ 80 | titleParameter, 81 | descParameter, 82 | limitParameter, 83 | offsetParameter, 84 | } 85 | 86 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 87 | 88 | // finish tool setup 89 | return Tool{ 90 | Name: cfg.Name, 91 | Kind: kind, 92 | Parameters: parameters, 93 | AuthRequired: cfg.AuthRequired, 94 | UseClientOAuth: s.UseClientOAuth, 95 | Client: s.Client, 96 | ApiSettings: s.ApiSettings, 97 | manifest: tools.Manifest{ 98 | Description: cfg.Description, 99 | Parameters: parameters.Manifest(), 100 | AuthRequired: cfg.AuthRequired, 101 | }, 102 | mcpManifest: mcpManifest, 103 | }, nil 104 | } 105 | 106 | // validate interface 107 | var _ tools.Tool = Tool{} 108 | 109 | type Tool struct { 110 | Name string `yaml:"name"` 111 | Kind string `yaml:"kind"` 112 | UseClientOAuth bool 113 | Client *v4.LookerSDK 114 | ApiSettings *rtl.ApiSettings 115 | AuthRequired []string `yaml:"authRequired"` 116 | Parameters tools.Parameters `yaml:"parameters"` 117 | manifest tools.Manifest 118 | mcpManifest tools.McpManifest 119 | } 120 | 121 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 122 | logger, err := util.LoggerFromContext(ctx) 123 | if err != nil { 124 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 125 | } 126 | paramsMap := params.AsMap() 127 | title := paramsMap["title"].(string) 128 | title_ptr := &title 129 | if *title_ptr == "" { 130 | title_ptr = nil 131 | } 132 | desc := paramsMap["desc"].(string) 133 | desc_ptr := &desc 134 | if *desc_ptr == "" { 135 | desc_ptr = nil 136 | } 137 | limit := int64(paramsMap["limit"].(int)) 138 | offset := int64(paramsMap["offset"].(int)) 139 | 140 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 141 | if err != nil { 142 | return nil, fmt.Errorf("error getting sdk: %w", err) 143 | } 144 | req := v4.RequestSearchLooks{ 145 | Title: title_ptr, 146 | Description: desc_ptr, 147 | Limit: &limit, 148 | Offset: &offset, 149 | } 150 | resp, err := sdk.SearchLooks(req, t.ApiSettings) 151 | if err != nil { 152 | return nil, fmt.Errorf("error making get_looks request: %s", err) 153 | } 154 | 155 | var data []any 156 | for _, v := range resp { 157 | logger.DebugContext(ctx, "Got response element of %v\n", v) 158 | vMap := make(map[string]any) 159 | if v.Id != nil { 160 | vMap["id"] = *v.Id 161 | } 162 | if v.Title != nil { 163 | vMap["title"] = *v.Title 164 | } 165 | if v.Description != nil { 166 | vMap["description"] = *v.Description 167 | } 168 | vMap["model_id"] = *v.Model.Id 169 | logger.DebugContext(ctx, "Converted to %v\n", vMap) 170 | data = append(data, vMap) 171 | } 172 | logger.DebugContext(ctx, "data = ", data) 173 | 174 | return data, nil 175 | } 176 | 177 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 178 | return tools.ParseParams(t.Parameters, data, claims) 179 | } 180 | 181 | func (t Tool) Manifest() tools.Manifest { 182 | return t.manifest 183 | } 184 | 185 | func (t Tool) McpManifest() tools.McpManifest { 186 | return t.mcpManifest 187 | } 188 | 189 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 190 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 191 | } 192 | 193 | func (t Tool) RequiresClientAuthorization() bool { 194 | return t.UseClientOAuth 195 | } 196 | ``` -------------------------------------------------------------------------------- /internal/sources/cloudsqlmysql/cloud_sql_mysql_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudsqlmysql_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/sources/cloudsqlmysql" 24 | "github.com/googleapis/genai-toolbox/internal/testutils" 25 | ) 26 | 27 | func TestParseFromYamlCloudSQLMySQL(t *testing.T) { 28 | tcs := []struct { 29 | desc string 30 | in string 31 | want server.SourceConfigs 32 | }{ 33 | { 34 | desc: "basic example", 35 | in: ` 36 | sources: 37 | my-mysql-instance: 38 | kind: cloud-sql-mysql 39 | project: my-project 40 | region: my-region 41 | instance: my-instance 42 | database: my_db 43 | user: my_user 44 | password: my_pass 45 | `, 46 | want: server.SourceConfigs{ 47 | "my-mysql-instance": cloudsqlmysql.Config{ 48 | Name: "my-mysql-instance", 49 | Kind: cloudsqlmysql.SourceKind, 50 | Project: "my-project", 51 | Region: "my-region", 52 | Instance: "my-instance", 53 | IPType: "public", 54 | Database: "my_db", 55 | User: "my_user", 56 | Password: "my_pass", 57 | }, 58 | }, 59 | }, 60 | { 61 | desc: "public ipType", 62 | in: ` 63 | sources: 64 | my-mysql-instance: 65 | kind: cloud-sql-mysql 66 | project: my-project 67 | region: my-region 68 | instance: my-instance 69 | ipType: Public 70 | database: my_db 71 | user: my_user 72 | password: my_pass 73 | `, 74 | want: server.SourceConfigs{ 75 | "my-mysql-instance": cloudsqlmysql.Config{ 76 | Name: "my-mysql-instance", 77 | Kind: cloudsqlmysql.SourceKind, 78 | Project: "my-project", 79 | Region: "my-region", 80 | Instance: "my-instance", 81 | IPType: "public", 82 | Database: "my_db", 83 | User: "my_user", 84 | Password: "my_pass", 85 | }, 86 | }, 87 | }, 88 | { 89 | desc: "private ipType", 90 | in: ` 91 | sources: 92 | my-mysql-instance: 93 | kind: cloud-sql-mysql 94 | project: my-project 95 | region: my-region 96 | instance: my-instance 97 | ipType: private 98 | database: my_db 99 | user: my_user 100 | password: my_pass 101 | `, 102 | want: server.SourceConfigs{ 103 | "my-mysql-instance": cloudsqlmysql.Config{ 104 | Name: "my-mysql-instance", 105 | Kind: cloudsqlmysql.SourceKind, 106 | Project: "my-project", 107 | Region: "my-region", 108 | Instance: "my-instance", 109 | IPType: "private", 110 | Database: "my_db", 111 | User: "my_user", 112 | Password: "my_pass", 113 | }, 114 | }, 115 | }, 116 | { 117 | desc: "psc ipType", 118 | in: ` 119 | sources: 120 | my-mysql-instance: 121 | kind: cloud-sql-mysql 122 | project: my-project 123 | region: my-region 124 | instance: my-instance 125 | ipType: psc 126 | database: my_db 127 | user: my_user 128 | password: my_pass 129 | `, 130 | want: server.SourceConfigs{ 131 | "my-mysql-instance": cloudsqlmysql.Config{ 132 | Name: "my-mysql-instance", 133 | Kind: cloudsqlmysql.SourceKind, 134 | Project: "my-project", 135 | Region: "my-region", 136 | Instance: "my-instance", 137 | IPType: "psc", 138 | Database: "my_db", 139 | User: "my_user", 140 | Password: "my_pass", 141 | }, 142 | }, 143 | }, 144 | } 145 | for _, tc := range tcs { 146 | t.Run(tc.desc, func(t *testing.T) { 147 | got := struct { 148 | Sources server.SourceConfigs `yaml:"sources"` 149 | }{} 150 | // Parse contents 151 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 152 | if err != nil { 153 | t.Fatalf("unable to unmarshal: %s", err) 154 | } 155 | if !cmp.Equal(tc.want, got.Sources) { 156 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 157 | } 158 | }) 159 | } 160 | 161 | } 162 | 163 | func TestFailParseFromYaml(t *testing.T) { 164 | tcs := []struct { 165 | desc string 166 | in string 167 | err string 168 | }{ 169 | { 170 | desc: "invalid ipType", 171 | in: ` 172 | sources: 173 | my-mysql-instance: 174 | kind: cloud-sql-mysql 175 | project: my-project 176 | region: my-region 177 | instance: my-instance 178 | ipType: fail 179 | database: my_db 180 | user: my_user 181 | password: my_pass 182 | `, 183 | err: "unable to parse source \"my-mysql-instance\" as \"cloud-sql-mysql\": ipType invalid: must be one of \"public\", \"private\", or \"psc\"", 184 | }, 185 | { 186 | desc: "extra field", 187 | in: ` 188 | sources: 189 | my-mysql-instance: 190 | kind: cloud-sql-mysql 191 | project: my-project 192 | region: my-region 193 | instance: my-instance 194 | database: my_db 195 | user: my_user 196 | password: my_pass 197 | foo: bar 198 | `, 199 | err: "unable to parse source \"my-mysql-instance\" as \"cloud-sql-mysql\": [2:1] unknown field \"foo\"\n 1 | database: my_db\n> 2 | foo: bar\n ^\n 3 | instance: my-instance\n 4 | kind: cloud-sql-mysql\n 5 | password: my_pass\n 6 | ", 200 | }, 201 | { 202 | desc: "missing required field", 203 | in: ` 204 | sources: 205 | my-mysql-instance: 206 | kind: cloud-sql-mysql 207 | region: my-region 208 | instance: my-instance 209 | database: my_db 210 | user: my_user 211 | password: my_pass 212 | `, 213 | err: "unable to parse source \"my-mysql-instance\" as \"cloud-sql-mysql\": Key: 'Config.Project' Error:Field validation for 'Project' failed on the 'required' tag", 214 | }, 215 | } 216 | for _, tc := range tcs { 217 | t.Run(tc.desc, func(t *testing.T) { 218 | got := struct { 219 | Sources server.SourceConfigs `yaml:"sources"` 220 | }{} 221 | // Parse contents 222 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 223 | if err == nil { 224 | t.Fatalf("expect parsing to fail") 225 | } 226 | errStr := err.Error() 227 | if errStr != tc.err { 228 | t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) 229 | } 230 | }) 231 | } 232 | } 233 | ``` -------------------------------------------------------------------------------- /internal/tools/sqlite/sqliteexecutesql/sqliteexecutesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sqliteexecutesql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "encoding/json" 21 | "fmt" 22 | 23 | yaml "github.com/goccy/go-yaml" 24 | "github.com/googleapis/genai-toolbox/internal/sources" 25 | "github.com/googleapis/genai-toolbox/internal/sources/sqlite" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | "github.com/googleapis/genai-toolbox/internal/util" 28 | ) 29 | 30 | const kind string = "sqlite-execute-sql" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | SQLiteDB() *sql.DB 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &sqlite.Source{} 52 | 53 | var compatibleSources = [...]string{sqlite.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" validate:"required"` 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 | // verify source exists 72 | rawS, ok := srcs[cfg.Source] 73 | if !ok { 74 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 75 | } 76 | 77 | // verify the source is compatible 78 | s, ok := rawS.(compatibleSource) 79 | if !ok { 80 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 81 | } 82 | 83 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 84 | parameters := tools.Parameters{sqlParameter} 85 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 86 | 87 | // finish tool setup 88 | t := Tool{ 89 | Name: cfg.Name, 90 | Kind: kind, 91 | Parameters: parameters, 92 | AuthRequired: cfg.AuthRequired, 93 | DB: s.SQLiteDB(), 94 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 95 | mcpManifest: mcpManifest, 96 | } 97 | return t, nil 98 | } 99 | 100 | // validate interface 101 | var _ tools.Tool = Tool{} 102 | 103 | type Tool struct { 104 | Name string `yaml:"name"` 105 | Kind string `yaml:"kind"` 106 | AuthRequired []string `yaml:"authRequired"` 107 | Parameters tools.Parameters `yaml:"parameters"` 108 | 109 | DB *sql.DB 110 | manifest tools.Manifest 111 | mcpManifest tools.McpManifest 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | sql, ok := params.AsMap()["sql"].(string) 116 | if !ok { 117 | return nil, fmt.Errorf("missing or invalid 'sql' parameter") 118 | } 119 | if sql == "" { 120 | return nil, fmt.Errorf("sql parameter cannot be empty") 121 | } 122 | 123 | // Log the query executed for debugging. 124 | logger, err := util.LoggerFromContext(ctx) 125 | if err != nil { 126 | return nil, fmt.Errorf("error getting logger: %s", err) 127 | } 128 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql) 129 | 130 | results, err := t.DB.QueryContext(ctx, sql) 131 | if err != nil { 132 | return nil, fmt.Errorf("unable to execute query: %w", err) 133 | } 134 | 135 | cols, err := results.Columns() 136 | if err != nil { 137 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 138 | } 139 | 140 | // The sqlite driver does not support ColumnTypes, so we can't get the 141 | // underlying database type of the columns. We'll have to rely on the 142 | // generic `any` type and then handle the JSON data separately. 143 | 144 | // create an array of values for each column, which can be re-used to scan each row 145 | rawValues := make([]any, len(cols)) 146 | values := make([]any, len(cols)) 147 | for i := range rawValues { 148 | values[i] = &rawValues[i] 149 | } 150 | defer results.Close() 151 | 152 | var out []any 153 | for results.Next() { 154 | err := results.Scan(values...) 155 | if err != nil { 156 | return nil, fmt.Errorf("unable to parse row: %w", err) 157 | } 158 | vMap := make(map[string]any) 159 | for i, name := range cols { 160 | val := rawValues[i] 161 | if val == nil { 162 | vMap[name] = nil 163 | continue 164 | } 165 | 166 | // Handle JSON data 167 | if jsonString, ok := val.(string); ok { 168 | var unmarshaledData any 169 | if json.Unmarshal([]byte(jsonString), &unmarshaledData) == nil { 170 | vMap[name] = unmarshaledData 171 | continue 172 | } 173 | } 174 | vMap[name] = val 175 | } 176 | out = append(out, vMap) 177 | } 178 | 179 | if err := results.Err(); err != nil { 180 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 181 | } 182 | 183 | if len(out) == 0 { 184 | return nil, nil 185 | } 186 | 187 | return out, nil 188 | } 189 | 190 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 191 | return tools.ParseParams(t.Parameters, data, claims) 192 | } 193 | 194 | func (t Tool) Manifest() tools.Manifest { 195 | return t.manifest 196 | } 197 | 198 | func (t Tool) McpManifest() tools.McpManifest { 199 | return t.mcpManifest 200 | } 201 | 202 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 203 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 204 | } 205 | 206 | func (t Tool) RequiresClientAuthorization() bool { 207 | return false 208 | } 209 | ``` -------------------------------------------------------------------------------- /internal/tools/cloudsql/cloudsqlcreateusers/cloudsqlcreateusers.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 cloudsqlcreateusers 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | sqladmin "google.golang.org/api/sqladmin/v1" 26 | ) 27 | 28 | const kind string = "cloud-sql-create-users" 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 | // Config defines the configuration for the create-user tool. 45 | type Config struct { 46 | Name string `yaml:"name" validate:"required"` 47 | Kind string `yaml:"kind" validate:"required"` 48 | Source string `yaml:"source" validate:"required"` 49 | Description string `yaml:"description"` 50 | AuthRequired []string `yaml:"authRequired"` 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("no source named %q configured", cfg.Source) 66 | } 67 | s, ok := rawS.(*cloudsqladmin.Source) 68 | if !ok { 69 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `cloud-sql-admin`", kind) 70 | } 71 | 72 | allParameters := tools.Parameters{ 73 | tools.NewStringParameter("project", "The project ID"), 74 | tools.NewStringParameter("instance", "The ID of the instance where the user will be created."), 75 | tools.NewStringParameter("name", "The name for the new user. Must be unique within the instance."), 76 | tools.NewStringParameterWithRequired("password", "A secure password for the new user. Not required for IAM users.", false), 77 | tools.NewBooleanParameter("iamUser", "Set to true to create a Cloud IAM user."), 78 | } 79 | paramManifest := allParameters.Manifest() 80 | 81 | description := cfg.Description 82 | if description == "" { 83 | description = "Creates a new user in a Cloud SQL instance. Both built-in and IAM users are supported. IAM users require an email account as the user name. IAM is the more secure and recommended way to manage users. The agent should always ask the user what type of user they want to create. For more information, see https://cloud.google.com/sql/docs/postgres/add-manage-iam-users" 84 | } 85 | mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters) 86 | 87 | return Tool{ 88 | Name: cfg.Name, 89 | Kind: kind, 90 | AuthRequired: cfg.AuthRequired, 91 | Source: s, 92 | AllParams: allParameters, 93 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 94 | mcpManifest: mcpManifest, 95 | }, nil 96 | } 97 | 98 | // Tool represents the create-user tool. 99 | type Tool struct { 100 | Name string `yaml:"name"` 101 | Kind string `yaml:"kind"` 102 | Description string `yaml:"description"` 103 | AuthRequired []string `yaml:"authRequired"` 104 | 105 | Source *cloudsqladmin.Source 106 | AllParams tools.Parameters `yaml:"allParams"` 107 | manifest tools.Manifest 108 | mcpManifest tools.McpManifest 109 | } 110 | 111 | // Invoke executes the tool's logic. 112 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 113 | paramsMap := params.AsMap() 114 | 115 | project, ok := paramsMap["project"].(string) 116 | if !ok { 117 | return nil, fmt.Errorf("missing 'project' parameter") 118 | } 119 | instance, ok := paramsMap["instance"].(string) 120 | if !ok { 121 | return nil, fmt.Errorf("missing 'instance' parameter") 122 | } 123 | name, ok := paramsMap["name"].(string) 124 | if !ok { 125 | return nil, fmt.Errorf("missing 'name' parameter") 126 | } 127 | 128 | iamUser, _ := paramsMap["iamUser"].(bool) 129 | 130 | user := sqladmin.User{ 131 | Name: name, 132 | } 133 | 134 | if iamUser { 135 | user.Type = "CLOUD_IAM_USER" 136 | } else { 137 | user.Type = "BUILT_IN" 138 | password, ok := paramsMap["password"].(string) 139 | if !ok || password == "" { 140 | return nil, fmt.Errorf("missing 'password' parameter for non-IAM user") 141 | } 142 | user.Password = password 143 | } 144 | 145 | service, err := t.Source.GetService(ctx, string(accessToken)) 146 | if err != nil { 147 | return nil, err 148 | } 149 | 150 | resp, err := service.Users.Insert(project, instance, &user).Do() 151 | if err != nil { 152 | return nil, fmt.Errorf("error creating user: %w", err) 153 | } 154 | 155 | return resp, nil 156 | } 157 | 158 | // ParseParams parses the parameters for the tool. 159 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 160 | return tools.ParseParams(t.AllParams, data, claims) 161 | } 162 | 163 | // Manifest returns the tool's manifest. 164 | func (t Tool) Manifest() tools.Manifest { 165 | return t.manifest 166 | } 167 | 168 | // McpManifest returns the tool's MCP manifest. 169 | func (t Tool) McpManifest() tools.McpManifest { 170 | return t.mcpManifest 171 | } 172 | 173 | // Authorized checks if the tool is authorized. 174 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 175 | return true 176 | } 177 | 178 | func (t Tool) RequiresClientAuthorization() bool { 179 | return t.Source.UseClientAuthorization() 180 | } 181 | ``` -------------------------------------------------------------------------------- /internal/server/mcp/v20250326/types.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 v20250326 16 | 17 | import ( 18 | "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" 19 | "github.com/googleapis/genai-toolbox/internal/tools" 20 | ) 21 | 22 | // SERVER_NAME is the server name used in Implementation. 23 | const SERVER_NAME = "Toolbox" 24 | 25 | // PROTOCOL_VERSION is the version of the MCP protocol in this package. 26 | const PROTOCOL_VERSION = "2025-03-26" 27 | 28 | // methods that are supported. 29 | const ( 30 | PING = "ping" 31 | TOOLS_LIST = "tools/list" 32 | TOOLS_CALL = "tools/call" 33 | ) 34 | 35 | /* Empty result */ 36 | 37 | // EmptyResult represents a response that indicates success but carries no data. 38 | type EmptyResult jsonrpc.Result 39 | 40 | /* Pagination */ 41 | 42 | // Cursor is an opaque token used to represent a cursor for pagination. 43 | type Cursor string 44 | 45 | type PaginatedRequest struct { 46 | jsonrpc.Request 47 | Params struct { 48 | // An opaque token representing the current pagination position. 49 | // If provided, the server should return results starting after this cursor. 50 | Cursor Cursor `json:"cursor,omitempty"` 51 | } `json:"params,omitempty"` 52 | } 53 | 54 | type PaginatedResult struct { 55 | jsonrpc.Result 56 | // An opaque token representing the pagination position after the last returned result. 57 | // If present, there may be more results available. 58 | NextCursor Cursor `json:"nextCursor,omitempty"` 59 | } 60 | 61 | /* Tools */ 62 | 63 | // Sent from the client to request a list of tools the server has. 64 | type ListToolsRequest struct { 65 | PaginatedRequest 66 | } 67 | 68 | // The server's response to a tools/list request from the client. 69 | type ListToolsResult struct { 70 | PaginatedResult 71 | Tools []tools.McpManifest `json:"tools"` 72 | } 73 | 74 | // Used by the client to invoke a tool provided by the server. 75 | type CallToolRequest struct { 76 | jsonrpc.Request 77 | Params struct { 78 | Name string `json:"name"` 79 | Arguments map[string]any `json:"arguments,omitempty"` 80 | } `json:"params,omitempty"` 81 | } 82 | 83 | // The sender or recipient of messages and data in a conversation. 84 | type Role string 85 | 86 | const ( 87 | RoleUser Role = "user" 88 | RoleAssistant Role = "assistant" 89 | ) 90 | 91 | // Base for objects that include optional annotations for the client. 92 | // The client can use annotations to inform how objects are used or displayed 93 | type Annotated struct { 94 | Annotations *struct { 95 | // Describes who the intended customer of this object or data is. 96 | // It can include multiple entries to indicate content useful for multiple 97 | // audiences (e.g., `["user", "assistant"]`). 98 | Audience []Role `json:"audience,omitempty"` 99 | // Describes how important this data is for operating the server. 100 | // 101 | // A value of 1 means "most important," and indicates that the data is 102 | // effectively required, while 0 means "least important," and indicates that 103 | // the data is entirely optional. 104 | // 105 | // @TJS-type number 106 | // @minimum 0 107 | // @maximum 1 108 | Priority float64 `json:"priority,omitempty"` 109 | } `json:"annotations,omitempty"` 110 | } 111 | 112 | // TextContent represents text provided to or from an LLM. 113 | type TextContent struct { 114 | Annotated 115 | Type string `json:"type"` 116 | // The text content of the message. 117 | Text string `json:"text"` 118 | } 119 | 120 | // The server's response to a tool call. 121 | // 122 | // Any errors that originate from the tool SHOULD be reported inside the result 123 | // object, with `isError` set to true, _not_ as an MCP protocol-level error 124 | // response. Otherwise, the LLM would not be able to see that an error occurred 125 | // and self-correct. 126 | // 127 | // However, any errors in _finding_ the tool, an error indicating that the 128 | // server does not support tool calls, or any other exceptional conditions, 129 | // should be reported as an MCP error response. 130 | type CallToolResult struct { 131 | jsonrpc.Result 132 | // Could be either a TextContent, ImageContent, or EmbeddedResources 133 | // For Toolbox, we will only be sending TextContent 134 | Content []TextContent `json:"content"` 135 | // Whether the tool call ended in an error. 136 | // If not set, this is assumed to be false (the call was successful). 137 | IsError bool `json:"isError,omitempty"` 138 | } 139 | 140 | // Additional properties describing a Tool to clients. 141 | // 142 | // NOTE: all properties in ToolAnnotations are **hints**. 143 | // They are not guaranteed to provide a faithful description of 144 | // tool behavior (including descriptive properties like `title`). 145 | // 146 | // Clients should never make tool use decisions based on ToolAnnotations 147 | // received from untrusted servers. 148 | type ToolAnnotations struct { 149 | // A human-readable title for the tool. 150 | Title string `json:"title,omitempty"` 151 | // If true, the tool does not modify its environment. 152 | // Default: false 153 | ReadOnlyHint bool `json:"readOnlyHint,omitempty"` 154 | // If true, the tool may perform destructive updates to its environment. 155 | // If false, the tool performs only additive updates. 156 | // (This property is meaningful only when `readOnlyHint == false`) 157 | // Default: true 158 | DestructiveHint bool `json:"destructiveHint,omitempty"` 159 | // If true, calling the tool repeatedly with the same arguments 160 | // will have no additional effect on the its environment. 161 | // (This property is meaningful only when `readOnlyHint == false`) 162 | // Default: false 163 | IdempotentHint bool `json:"idempotentHint,omitempty"` 164 | // If true, this tool may interact with an "open world" of external 165 | // entities. If false, the tool's domain of interaction is closed. 166 | // For example, the world of a web search tool is open, whereas that 167 | // of a memory tool is not. 168 | // Default: true 169 | OpenWorldHint bool `json:"openWorldHint,omitempty"` 170 | } 171 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetdashboards/lookergetdashboards.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 lookergetdashboards 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-dashboards" 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 | titleParameter := tools.NewStringParameterWithDefault("title", "", "The title of the dashboard.") 76 | descParameter := tools.NewStringParameterWithDefault("desc", "", "The description of the dashboard.") 77 | limitParameter := tools.NewIntParameterWithDefault("limit", 100, "The number of dashboards to fetch. Default 100") 78 | offsetParameter := tools.NewIntParameterWithDefault("offset", 0, "The number of dashboards to skip before fetching. Default 0") 79 | parameters := tools.Parameters{ 80 | titleParameter, 81 | descParameter, 82 | limitParameter, 83 | offsetParameter, 84 | } 85 | 86 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 87 | 88 | // finish tool setup 89 | return Tool{ 90 | Name: cfg.Name, 91 | Kind: kind, 92 | Parameters: parameters, 93 | AuthRequired: cfg.AuthRequired, 94 | UseClientOAuth: s.UseClientOAuth, 95 | Client: s.Client, 96 | ApiSettings: s.ApiSettings, 97 | manifest: tools.Manifest{ 98 | Description: cfg.Description, 99 | Parameters: parameters.Manifest(), 100 | AuthRequired: cfg.AuthRequired, 101 | }, 102 | mcpManifest: mcpManifest, 103 | }, nil 104 | } 105 | 106 | // validate interface 107 | var _ tools.Tool = Tool{} 108 | 109 | type Tool struct { 110 | Name string `yaml:"name"` 111 | Kind string `yaml:"kind"` 112 | UseClientOAuth bool 113 | Client *v4.LookerSDK 114 | ApiSettings *rtl.ApiSettings 115 | AuthRequired []string `yaml:"authRequired"` 116 | Parameters tools.Parameters `yaml:"parameters"` 117 | manifest tools.Manifest 118 | mcpManifest tools.McpManifest 119 | } 120 | 121 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 122 | logger, err := util.LoggerFromContext(ctx) 123 | if err != nil { 124 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 125 | } 126 | paramsMap := params.AsMap() 127 | title := paramsMap["title"].(string) 128 | title_ptr := &title 129 | if *title_ptr == "" { 130 | title_ptr = nil 131 | } 132 | desc := paramsMap["desc"].(string) 133 | desc_ptr := &desc 134 | if *desc_ptr == "" { 135 | desc_ptr = nil 136 | } 137 | limit := int64(paramsMap["limit"].(int)) 138 | offset := int64(paramsMap["offset"].(int)) 139 | 140 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 141 | if err != nil { 142 | return nil, fmt.Errorf("error getting sdk: %w", err) 143 | } 144 | req := v4.RequestSearchDashboards{ 145 | Title: title_ptr, 146 | Description: desc_ptr, 147 | Limit: &limit, 148 | Offset: &offset, 149 | } 150 | logger.ErrorContext(ctx, "Making request %v", req) 151 | resp, err := sdk.SearchDashboards(req, t.ApiSettings) 152 | if err != nil { 153 | return nil, fmt.Errorf("error making get_dashboards request: %s", err) 154 | } 155 | logger.ErrorContext(ctx, "Got response %v", resp) 156 | var data []any 157 | for _, v := range resp { 158 | logger.DebugContext(ctx, "Got response element of %v\n", v) 159 | vMap := make(map[string]any) 160 | if v.Id != nil { 161 | vMap["id"] = *v.Id 162 | } 163 | if v.Title != nil { 164 | vMap["title"] = *v.Title 165 | } 166 | if v.Description != nil { 167 | vMap["description"] = *v.Description 168 | } 169 | logger.DebugContext(ctx, "Converted to %v\n", vMap) 170 | data = append(data, vMap) 171 | } 172 | logger.DebugContext(ctx, "data = ", data) 173 | 174 | return data, nil 175 | } 176 | 177 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 178 | return tools.ParseParams(t.Parameters, data, claims) 179 | } 180 | 181 | func (t Tool) Manifest() tools.Manifest { 182 | return t.manifest 183 | } 184 | 185 | func (t Tool) McpManifest() tools.McpManifest { 186 | return t.mcpManifest 187 | } 188 | 189 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 190 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 191 | } 192 | 193 | func (t Tool) RequiresClientAuthorization() bool { 194 | return t.UseClientOAuth 195 | } 196 | ``` -------------------------------------------------------------------------------- /internal/tools/clickhouse/clickhousesql/clickhousesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package clickhouse 16 | 17 | import ( 18 | "context" 19 | "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/tools" 25 | ) 26 | 27 | type compatibleSource interface { 28 | ClickHousePool() *sql.DB 29 | } 30 | 31 | var compatibleSources = []string{"clickhouse"} 32 | 33 | const sqlKind string = "clickhouse-sql" 34 | 35 | func init() { 36 | if !tools.Register(sqlKind, newSQLConfig) { 37 | panic(fmt.Sprintf("tool kind %q already registered", sqlKind)) 38 | } 39 | } 40 | 41 | func newSQLConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 42 | actual := Config{Name: name} 43 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 44 | return nil, err 45 | } 46 | return actual, nil 47 | } 48 | 49 | type Config struct { 50 | Name string `yaml:"name" validate:"required"` 51 | Kind string `yaml:"kind" validate:"required"` 52 | Source string `yaml:"source" validate:"required"` 53 | Description string `yaml:"description" validate:"required"` 54 | Statement string `yaml:"statement" validate:"required"` 55 | AuthRequired []string `yaml:"authRequired"` 56 | Parameters tools.Parameters `yaml:"parameters"` 57 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 58 | } 59 | 60 | var _ tools.ToolConfig = Config{} 61 | 62 | func (cfg Config) ToolConfigKind() string { 63 | return sqlKind 64 | } 65 | 66 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 67 | rawS, ok := srcs[cfg.Source] 68 | if !ok { 69 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 70 | } 71 | 72 | s, ok := rawS.(compatibleSource) 73 | if !ok { 74 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", sqlKind, compatibleSources) 75 | } 76 | 77 | allParameters, paramManifest, _ := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 78 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 79 | 80 | t := Tool{ 81 | Name: cfg.Name, 82 | Kind: sqlKind, 83 | Parameters: cfg.Parameters, 84 | TemplateParameters: cfg.TemplateParameters, 85 | AllParams: allParameters, 86 | Statement: cfg.Statement, 87 | AuthRequired: cfg.AuthRequired, 88 | Pool: s.ClickHousePool(), 89 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 90 | mcpManifest: mcpManifest, 91 | } 92 | return t, nil 93 | } 94 | 95 | var _ tools.Tool = Tool{} 96 | 97 | type Tool struct { 98 | Name string `yaml:"name"` 99 | Kind string `yaml:"kind"` 100 | AuthRequired []string `yaml:"authRequired"` 101 | Parameters tools.Parameters `yaml:"parameters"` 102 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 103 | AllParams tools.Parameters `yaml:"allParams"` 104 | 105 | Pool *sql.DB 106 | Statement string 107 | manifest tools.Manifest 108 | mcpManifest tools.McpManifest 109 | } 110 | 111 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, token tools.AccessToken) (any, error) { 112 | paramsMap := params.AsMap() 113 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 114 | if err != nil { 115 | return nil, fmt.Errorf("unable to extract template params: %w", err) 116 | } 117 | 118 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 119 | if err != nil { 120 | return nil, fmt.Errorf("unable to extract standard params: %w", err) 121 | } 122 | 123 | sliceParams := newParams.AsSlice() 124 | results, err := t.Pool.QueryContext(ctx, newStatement, sliceParams...) 125 | if err != nil { 126 | return nil, fmt.Errorf("unable to execute query: %w", err) 127 | } 128 | 129 | cols, err := results.Columns() 130 | if err != nil { 131 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 132 | } 133 | 134 | rawValues := make([]any, len(cols)) 135 | values := make([]any, len(cols)) 136 | for i := range rawValues { 137 | values[i] = &rawValues[i] 138 | } 139 | 140 | colTypes, err := results.ColumnTypes() 141 | if err != nil { 142 | return nil, fmt.Errorf("unable to get column types: %w", err) 143 | } 144 | 145 | var out []any 146 | for results.Next() { 147 | err := results.Scan(values...) 148 | if err != nil { 149 | return nil, fmt.Errorf("unable to parse row: %w", err) 150 | } 151 | vMap := make(map[string]any) 152 | for i, name := range cols { 153 | switch colTypes[i].DatabaseTypeName() { 154 | case "String", "FixedString": 155 | if rawValues[i] != nil { 156 | // Handle potential []byte to string conversion if needed 157 | if b, ok := rawValues[i].([]byte); ok { 158 | vMap[name] = string(b) 159 | } else { 160 | vMap[name] = rawValues[i] 161 | } 162 | } else { 163 | vMap[name] = nil 164 | } 165 | default: 166 | vMap[name] = rawValues[i] 167 | } 168 | } 169 | out = append(out, vMap) 170 | } 171 | 172 | err = results.Close() 173 | if err != nil { 174 | return nil, fmt.Errorf("unable to close rows: %w", err) 175 | } 176 | 177 | if err := results.Err(); err != nil { 178 | return nil, fmt.Errorf("errors encountered by results.Scan: %w", err) 179 | } 180 | 181 | return out, nil 182 | } 183 | 184 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 185 | return tools.ParseParams(t.AllParams, data, claims) 186 | } 187 | 188 | func (t Tool) Manifest() tools.Manifest { 189 | return t.manifest 190 | } 191 | 192 | func (t Tool) McpManifest() tools.McpManifest { 193 | return t.mcpManifest 194 | } 195 | 196 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 197 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 198 | } 199 | 200 | func (t Tool) RequiresClientAuthorization() bool { 201 | return false 202 | } 203 | ``` -------------------------------------------------------------------------------- /internal/tools/spanner/spannersql/spanner_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 spannersql_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/spanner/spannersql" 26 | ) 27 | 28 | func TestParseFromYamlSpanner(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: spanner-sql 44 | source: my-pg-instance 45 | description: some description 46 | statement: | 47 | SELECT * FROM SQL_STATEMENT; 48 | parameters: 49 | - name: country 50 | type: string 51 | description: some description 52 | `, 53 | want: server.ToolConfigs{ 54 | "example_tool": spannersql.Config{ 55 | Name: "example_tool", 56 | Kind: "spanner-sql", 57 | Source: "my-pg-instance", 58 | Description: "some description", 59 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 60 | AuthRequired: []string{}, 61 | Parameters: []tools.Parameter{ 62 | tools.NewStringParameter("country", "some description"), 63 | }, 64 | }, 65 | }, 66 | }, 67 | { 68 | desc: "read only set to true", 69 | in: ` 70 | tools: 71 | example_tool: 72 | kind: spanner-sql 73 | source: my-pg-instance 74 | description: some description 75 | readOnly: true 76 | statement: | 77 | SELECT * FROM SQL_STATEMENT; 78 | parameters: 79 | - name: country 80 | type: string 81 | description: some description 82 | `, 83 | want: server.ToolConfigs{ 84 | "example_tool": spannersql.Config{ 85 | Name: "example_tool", 86 | Kind: "spanner-sql", 87 | Source: "my-pg-instance", 88 | Description: "some description", 89 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 90 | ReadOnly: true, 91 | AuthRequired: []string{}, 92 | Parameters: []tools.Parameter{ 93 | tools.NewStringParameter("country", "some description"), 94 | }, 95 | }, 96 | }, 97 | }, 98 | } 99 | for _, tc := range tcs { 100 | t.Run(tc.desc, func(t *testing.T) { 101 | got := struct { 102 | Tools server.ToolConfigs `yaml:"tools"` 103 | }{} 104 | // Parse contents 105 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 106 | if err != nil { 107 | t.Fatalf("unable to unmarshal: %s", err) 108 | } 109 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 110 | t.Fatalf("incorrect parse: diff %v", diff) 111 | } 112 | }) 113 | } 114 | 115 | } 116 | 117 | func TestParseFromYamlWithTemplateParamsSpanner(t *testing.T) { 118 | ctx, err := testutils.ContextWithNewLogger() 119 | if err != nil { 120 | t.Fatalf("unexpected error: %s", err) 121 | } 122 | tcs := []struct { 123 | desc string 124 | in string 125 | want server.ToolConfigs 126 | }{ 127 | { 128 | desc: "basic example", 129 | in: ` 130 | tools: 131 | example_tool: 132 | kind: spanner-sql 133 | source: my-pg-instance 134 | description: some description 135 | statement: | 136 | SELECT * FROM SQL_STATEMENT; 137 | parameters: 138 | - name: country 139 | type: string 140 | description: some description 141 | templateParameters: 142 | - name: tableName 143 | type: string 144 | description: The table to select hotels from. 145 | - name: fieldArray 146 | type: array 147 | description: The columns to return for the query. 148 | items: 149 | name: column 150 | type: string 151 | description: A column name that will be returned from the query. 152 | `, 153 | want: server.ToolConfigs{ 154 | "example_tool": spannersql.Config{ 155 | Name: "example_tool", 156 | Kind: "spanner-sql", 157 | Source: "my-pg-instance", 158 | Description: "some description", 159 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 160 | AuthRequired: []string{}, 161 | Parameters: []tools.Parameter{ 162 | tools.NewStringParameter("country", "some description"), 163 | }, 164 | TemplateParameters: []tools.Parameter{ 165 | tools.NewStringParameter("tableName", "The table to select hotels from."), 166 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 167 | }, 168 | }, 169 | }, 170 | }, 171 | { 172 | desc: "read only set to true", 173 | in: ` 174 | tools: 175 | example_tool: 176 | kind: spanner-sql 177 | source: my-pg-instance 178 | description: some description 179 | readOnly: true 180 | statement: | 181 | SELECT * FROM SQL_STATEMENT; 182 | parameters: 183 | - name: country 184 | type: string 185 | description: some description 186 | `, 187 | want: server.ToolConfigs{ 188 | "example_tool": spannersql.Config{ 189 | Name: "example_tool", 190 | Kind: "spanner-sql", 191 | Source: "my-pg-instance", 192 | Description: "some description", 193 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 194 | ReadOnly: true, 195 | AuthRequired: []string{}, 196 | Parameters: []tools.Parameter{ 197 | tools.NewStringParameter("country", "some description"), 198 | }, 199 | }, 200 | }, 201 | }, 202 | } 203 | for _, tc := range tcs { 204 | t.Run(tc.desc, func(t *testing.T) { 205 | got := struct { 206 | Tools server.ToolConfigs `yaml:"tools"` 207 | }{} 208 | // Parse contents 209 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 210 | if err != nil { 211 | t.Fatalf("unable to unmarshal: %s", err) 212 | } 213 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 214 | t.Fatalf("incorrect parse: diff %v", diff) 215 | } 216 | }) 217 | } 218 | 219 | } 220 | ``` -------------------------------------------------------------------------------- /internal/sources/looker/looker.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 looker 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "time" 20 | 21 | geminidataanalytics "cloud.google.com/go/geminidataanalytics/apiv1beta" 22 | "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/util" 25 | "go.opentelemetry.io/otel/trace" 26 | "golang.org/x/oauth2" 27 | "golang.org/x/oauth2/google" 28 | 29 | "github.com/looker-open-source/sdk-codegen/go/rtl" 30 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 31 | ) 32 | 33 | const SourceKind string = "looker" 34 | 35 | // validate interface 36 | var _ sources.SourceConfig = Config{} 37 | 38 | func init() { 39 | if !sources.Register(SourceKind, newConfig) { 40 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 41 | } 42 | } 43 | 44 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 45 | actual := Config{ 46 | Name: name, 47 | SslVerification: true, 48 | Timeout: "600s", 49 | UseClientOAuth: false, 50 | ShowHiddenModels: true, 51 | ShowHiddenExplores: true, 52 | ShowHiddenFields: true, 53 | Location: "us", 54 | } // Default Ssl,timeout, ShowHidden 55 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 56 | return nil, err 57 | } 58 | return actual, nil 59 | } 60 | 61 | type Config struct { 62 | Name string `yaml:"name" validate:"required"` 63 | Kind string `yaml:"kind" validate:"required"` 64 | BaseURL string `yaml:"base_url" validate:"required"` 65 | ClientId string `yaml:"client_id"` 66 | ClientSecret string `yaml:"client_secret"` 67 | SslVerification bool `yaml:"verify_ssl"` 68 | UseClientOAuth bool `yaml:"use_client_oauth"` 69 | Timeout string `yaml:"timeout"` 70 | ShowHiddenModels bool `yaml:"show_hidden_models"` 71 | ShowHiddenExplores bool `yaml:"show_hidden_explores"` 72 | ShowHiddenFields bool `yaml:"show_hidden_fields"` 73 | Project string `yaml:"project"` 74 | Location string `yaml:"location"` 75 | } 76 | 77 | func (r Config) SourceConfigKind() string { 78 | return SourceKind 79 | } 80 | 81 | // Initialize initializes a Looker Source instance. 82 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 83 | logger, err := util.LoggerFromContext(ctx) 84 | if err != nil { 85 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 86 | } 87 | 88 | userAgent, err := util.UserAgentFromContext(ctx) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | duration, err := time.ParseDuration(r.Timeout) 94 | if err != nil { 95 | return nil, fmt.Errorf("unable to parse Timeout string as time.Duration: %s", err) 96 | } 97 | 98 | if !r.SslVerification { 99 | logger.WarnContext(ctx, "Insecure HTTP is enabled for Looker source %s. TLS certificate verification is skipped.\n", r.Name) 100 | } 101 | cfg := rtl.ApiSettings{ 102 | AgentTag: userAgent, 103 | BaseUrl: r.BaseURL, 104 | ApiVersion: "4.0", 105 | VerifySsl: r.SslVerification, 106 | Timeout: int32(duration.Seconds()), 107 | ClientId: r.ClientId, 108 | ClientSecret: r.ClientSecret, 109 | } 110 | 111 | var tokenSource oauth2.TokenSource 112 | tokenSource, _ = initGoogleCloudConnection(ctx) 113 | 114 | s := &Source{ 115 | Name: r.Name, 116 | Kind: SourceKind, 117 | Timeout: r.Timeout, 118 | UseClientOAuth: r.UseClientOAuth, 119 | ApiSettings: &cfg, 120 | ShowHiddenModels: r.ShowHiddenModels, 121 | ShowHiddenExplores: r.ShowHiddenExplores, 122 | ShowHiddenFields: r.ShowHiddenFields, 123 | Project: r.Project, 124 | Location: r.Location, 125 | TokenSource: tokenSource, 126 | } 127 | 128 | if !r.UseClientOAuth { 129 | if r.ClientId == "" || r.ClientSecret == "" { 130 | return nil, fmt.Errorf("client_id and client_secret need to be specified") 131 | } 132 | s.Client = v4.NewLookerSDK(rtl.NewAuthSession(cfg)) 133 | resp, err := s.Client.Me("", s.ApiSettings) 134 | if err != nil { 135 | return nil, fmt.Errorf("incorrect settings: %w", err) 136 | } 137 | logger.DebugContext(ctx, fmt.Sprintf("logged in as %s %s", *resp.FirstName, *resp.LastName)) 138 | } 139 | 140 | return s, nil 141 | 142 | } 143 | 144 | var _ sources.Source = &Source{} 145 | 146 | type Source struct { 147 | Name string `yaml:"name"` 148 | Kind string `yaml:"kind"` 149 | Timeout string `yaml:"timeout"` 150 | Client *v4.LookerSDK 151 | ApiSettings *rtl.ApiSettings 152 | UseClientOAuth bool `yaml:"use_client_oauth"` 153 | ShowHiddenModels bool `yaml:"show_hidden_models"` 154 | ShowHiddenExplores bool `yaml:"show_hidden_explores"` 155 | ShowHiddenFields bool `yaml:"show_hidden_fields"` 156 | Project string `yaml:"project"` 157 | Location string `yaml:"location"` 158 | TokenSource oauth2.TokenSource 159 | } 160 | 161 | func (s *Source) SourceKind() string { 162 | return SourceKind 163 | } 164 | 165 | func (s *Source) GetApiSettings() *rtl.ApiSettings { 166 | return s.ApiSettings 167 | } 168 | 169 | func (s *Source) UseClientAuthorization() bool { 170 | return s.UseClientOAuth 171 | } 172 | 173 | func (s *Source) GoogleCloudProject() string { 174 | return s.Project 175 | } 176 | 177 | func (s *Source) GoogleCloudLocation() string { 178 | return s.Location 179 | } 180 | 181 | func (s *Source) GoogleCloudTokenSource() oauth2.TokenSource { 182 | return s.TokenSource 183 | } 184 | 185 | func (s *Source) GoogleCloudTokenSourceWithScope(ctx context.Context, scope string) (oauth2.TokenSource, error) { 186 | return google.DefaultTokenSource(ctx, scope) 187 | } 188 | 189 | func initGoogleCloudConnection(ctx context.Context) (oauth2.TokenSource, error) { 190 | cred, err := google.FindDefaultCredentials(ctx, geminidataanalytics.DefaultAuthScopes()...) 191 | if err != nil { 192 | return nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", geminidataanalytics.DefaultAuthScopes(), err) 193 | } 194 | 195 | return cred.TokenSource, nil 196 | } 197 | ``` -------------------------------------------------------------------------------- /internal/sources/trino/trino_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 trino 16 | 17 | import ( 18 | "testing" 19 | 20 | "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 | ) 25 | 26 | func TestBuildTrinoDSN(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | host string 30 | port string 31 | user string 32 | password string 33 | catalog string 34 | schema string 35 | queryTimeout string 36 | accessToken string 37 | kerberosEnabled bool 38 | sslEnabled bool 39 | want string 40 | wantErr bool 41 | }{ 42 | { 43 | name: "basic configuration", 44 | host: "localhost", 45 | port: "8080", 46 | user: "testuser", 47 | catalog: "hive", 48 | schema: "default", 49 | want: "http://testuser@localhost:8080?catalog=hive&schema=default", 50 | wantErr: false, 51 | }, 52 | { 53 | name: "with password", 54 | host: "localhost", 55 | port: "8080", 56 | user: "testuser", 57 | password: "testpass", 58 | catalog: "hive", 59 | schema: "default", 60 | want: "http://testuser:testpass@localhost:8080?catalog=hive&schema=default", 61 | wantErr: false, 62 | }, 63 | { 64 | name: "with SSL", 65 | host: "localhost", 66 | port: "8443", 67 | user: "testuser", 68 | catalog: "hive", 69 | schema: "default", 70 | sslEnabled: true, 71 | want: "https://testuser@localhost:8443?catalog=hive&schema=default", 72 | wantErr: false, 73 | }, 74 | { 75 | name: "with access token", 76 | host: "localhost", 77 | port: "8080", 78 | user: "testuser", 79 | catalog: "hive", 80 | schema: "default", 81 | accessToken: "jwt-token-here", 82 | want: "http://testuser@localhost:8080?accessToken=jwt-token-here&catalog=hive&schema=default", 83 | wantErr: false, 84 | }, 85 | { 86 | name: "with kerberos", 87 | host: "localhost", 88 | port: "8080", 89 | user: "testuser", 90 | catalog: "hive", 91 | schema: "default", 92 | kerberosEnabled: true, 93 | want: "http://testuser@localhost:8080?KerberosEnabled=true&catalog=hive&schema=default", 94 | wantErr: false, 95 | }, 96 | { 97 | name: "with query timeout", 98 | host: "localhost", 99 | port: "8080", 100 | user: "testuser", 101 | catalog: "hive", 102 | schema: "default", 103 | queryTimeout: "30m", 104 | want: "http://testuser@localhost:8080?catalog=hive&queryTimeout=30m&schema=default", 105 | wantErr: false, 106 | }, 107 | { 108 | name: "anonymous access (empty user)", 109 | host: "localhost", 110 | port: "8080", 111 | catalog: "hive", 112 | schema: "default", 113 | want: "http://localhost:8080?catalog=hive&schema=default", 114 | wantErr: false, 115 | }, 116 | } 117 | 118 | for _, tt := range tests { 119 | t.Run(tt.name, func(t *testing.T) { 120 | got, err := buildTrinoDSN(tt.host, tt.port, tt.user, tt.password, tt.catalog, tt.schema, tt.queryTimeout, tt.accessToken, tt.kerberosEnabled, tt.sslEnabled) 121 | if (err != nil) != tt.wantErr { 122 | t.Errorf("buildTrinoDSN() error = %v, wantErr %v", err, tt.wantErr) 123 | return 124 | } 125 | if diff := cmp.Diff(tt.want, got); diff != "" { 126 | t.Errorf("buildTrinoDSN() mismatch (-want +got):\n%s", diff) 127 | } 128 | }) 129 | } 130 | } 131 | 132 | func TestParseFromYamlTrino(t *testing.T) { 133 | tcs := []struct { 134 | desc string 135 | in string 136 | want server.SourceConfigs 137 | }{ 138 | { 139 | desc: "basic example", 140 | in: ` 141 | sources: 142 | my-trino-instance: 143 | kind: trino 144 | host: localhost 145 | port: "8080" 146 | user: testuser 147 | catalog: hive 148 | schema: default 149 | `, 150 | want: server.SourceConfigs{ 151 | "my-trino-instance": Config{ 152 | Name: "my-trino-instance", 153 | Kind: SourceKind, 154 | Host: "localhost", 155 | Port: "8080", 156 | User: "testuser", 157 | Catalog: "hive", 158 | Schema: "default", 159 | }, 160 | }, 161 | }, 162 | { 163 | desc: "example with optional fields", 164 | in: ` 165 | sources: 166 | my-trino-instance: 167 | kind: trino 168 | host: localhost 169 | port: "8443" 170 | user: testuser 171 | password: testpass 172 | catalog: hive 173 | schema: default 174 | queryTimeout: "30m" 175 | accessToken: "jwt-token-here" 176 | kerberosEnabled: true 177 | sslEnabled: true 178 | `, 179 | want: server.SourceConfigs{ 180 | "my-trino-instance": Config{ 181 | Name: "my-trino-instance", 182 | Kind: SourceKind, 183 | Host: "localhost", 184 | Port: "8443", 185 | User: "testuser", 186 | Password: "testpass", 187 | Catalog: "hive", 188 | Schema: "default", 189 | QueryTimeout: "30m", 190 | AccessToken: "jwt-token-here", 191 | KerberosEnabled: true, 192 | SSLEnabled: true, 193 | }, 194 | }, 195 | }, 196 | { 197 | desc: "anonymous access without user", 198 | in: ` 199 | sources: 200 | my-trino-anonymous: 201 | kind: trino 202 | host: localhost 203 | port: "8080" 204 | catalog: hive 205 | schema: default 206 | `, 207 | want: server.SourceConfigs{ 208 | "my-trino-anonymous": Config{ 209 | Name: "my-trino-anonymous", 210 | Kind: SourceKind, 211 | Host: "localhost", 212 | Port: "8080", 213 | Catalog: "hive", 214 | Schema: "default", 215 | }, 216 | }, 217 | }, 218 | } 219 | for _, tc := range tcs { 220 | t.Run(tc.desc, func(t *testing.T) { 221 | got := struct { 222 | Sources server.SourceConfigs `yaml:"sources"` 223 | }{} 224 | // Parse contents 225 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 226 | if err != nil { 227 | t.Fatalf("unable to unmarshal: %s", err) 228 | } 229 | if !cmp.Equal(tc.want, got.Sources) { 230 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 231 | } 232 | }) 233 | } 234 | } 235 | ``` -------------------------------------------------------------------------------- /internal/tools/valkey/valkey.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 valkey 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/googleapis/genai-toolbox/internal/sources" 22 | valkeysrc "github.com/googleapis/genai-toolbox/internal/sources/valkey" 23 | "github.com/googleapis/genai-toolbox/internal/tools" 24 | "github.com/valkey-io/valkey-go" 25 | ) 26 | 27 | const kind string = "valkey" 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 | type compatibleSource interface { 44 | ValkeyClient() valkey.Client 45 | } 46 | 47 | // validate compatible sources are still compatible 48 | var _ compatibleSource = &valkeysrc.Source{} 49 | 50 | var compatibleSources = [...]string{valkeysrc.SourceKind, valkeysrc.SourceKind} 51 | 52 | type Config struct { 53 | Name string `yaml:"name" validate:"required"` 54 | Kind string `yaml:"kind" validate:"required"` 55 | Source string `yaml:"source" validate:"required"` 56 | Description string `yaml:"description" validate:"required"` 57 | Commands [][]string `yaml:"commands" validate:"required"` 58 | AuthRequired []string `yaml:"authRequired"` 59 | Parameters tools.Parameters `yaml:"parameters"` 60 | } 61 | 62 | // validate interface 63 | var _ tools.ToolConfig = Config{} 64 | 65 | func (cfg Config) ToolConfigKind() string { 66 | return kind 67 | } 68 | 69 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 70 | // verify source exists 71 | rawS, ok := srcs[cfg.Source] 72 | if !ok { 73 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 74 | } 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 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, cfg.Parameters) 83 | 84 | // finish tool setup 85 | t := Tool{ 86 | Name: cfg.Name, 87 | Kind: kind, 88 | Parameters: cfg.Parameters, 89 | Commands: cfg.Commands, 90 | AuthRequired: cfg.AuthRequired, 91 | Client: s.ValkeyClient(), 92 | manifest: tools.Manifest{Description: cfg.Description, Parameters: cfg.Parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 93 | mcpManifest: mcpManifest, 94 | } 95 | return t, 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 | AuthRequired []string `yaml:"authRequired"` 105 | Parameters tools.Parameters `yaml:"parameters"` 106 | 107 | Client valkey.Client 108 | Commands [][]string 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 | // Replace parameters 115 | commands, err := replaceCommandsParams(t.Commands, t.Parameters, params) 116 | if err != nil { 117 | return nil, fmt.Errorf("error replacing commands' parameters: %s", err) 118 | } 119 | 120 | // Build commands 121 | builtCmds := make(valkey.Commands, len(commands)) 122 | 123 | for i, cmd := range commands { 124 | builtCmds[i] = t.Client.B().Arbitrary(cmd...).Build() 125 | } 126 | 127 | if len(builtCmds) == 0 { 128 | return nil, fmt.Errorf("no valid commands were built to execute") 129 | } 130 | 131 | // Execute commands 132 | responses := t.Client.DoMulti(ctx, builtCmds...) 133 | 134 | // Parse responses 135 | out := make([]any, len(t.Commands)) 136 | for i, resp := range responses { 137 | if err := resp.Error(); err != nil { 138 | // Add error from each command to `errSum` 139 | out[i] = fmt.Sprintf("error from executing command at index %d: %s", i, err) 140 | continue 141 | } 142 | val, err := resp.ToAny() 143 | if err != nil { 144 | out[i] = fmt.Sprintf("error parsing response: %s", err) 145 | continue 146 | } 147 | out[i] = val 148 | } 149 | 150 | return out, nil 151 | } 152 | 153 | // replaceCommandsParams is a helper function to replace parameters in the commands 154 | func replaceCommandsParams(commands [][]string, params tools.Parameters, paramValues tools.ParamValues) ([][]string, error) { 155 | paramMap := paramValues.AsMapWithDollarPrefix() 156 | typeMap := make(map[string]string, len(params)) 157 | for _, p := range params { 158 | placeholder := "$" + p.GetName() 159 | typeMap[placeholder] = p.GetType() 160 | } 161 | 162 | newCommands := make([][]string, len(commands)) 163 | for i, cmd := range commands { 164 | newCmd := make([]string, 0) 165 | for _, part := range cmd { 166 | v, ok := paramMap[part] 167 | if !ok { 168 | // Command part is not a Parameter placeholder 169 | newCmd = append(newCmd, part) 170 | continue 171 | } 172 | if typeMap[part] == "array" { 173 | for _, item := range v.([]any) { 174 | // Nested arrays will only be expanded once 175 | // e.g., [A, [B, C]] --> ["A", "[B C]"] 176 | newCmd = append(newCmd, fmt.Sprintf("%s", item)) 177 | } 178 | continue 179 | } 180 | newCmd = append(newCmd, fmt.Sprintf("%s", v)) 181 | } 182 | newCommands[i] = newCmd 183 | } 184 | return newCommands, nil 185 | } 186 | 187 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 188 | return tools.ParseParams(t.Parameters, data, claims) 189 | } 190 | 191 | func (t Tool) Manifest() tools.Manifest { 192 | return t.manifest 193 | } 194 | 195 | func (t Tool) McpManifest() tools.McpManifest { 196 | return t.mcpManifest 197 | } 198 | 199 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 200 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 201 | } 202 | 203 | func (t Tool) RequiresClientAuthorization() bool { 204 | return false 205 | } 206 | ``` -------------------------------------------------------------------------------- /internal/tools/trino/trinosql/trinosql.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 trinosql 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-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 | Statement string `yaml:"statement" validate:"required"` 59 | AuthRequired []string `yaml:"authRequired"` 60 | Parameters tools.Parameters `yaml:"parameters"` 61 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 62 | } 63 | 64 | // validate interface 65 | var _ tools.ToolConfig = Config{} 66 | 67 | func (cfg Config) ToolConfigKind() string { 68 | return kind 69 | } 70 | 71 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 72 | // verify source exists 73 | rawS, ok := srcs[cfg.Source] 74 | if !ok { 75 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 76 | } 77 | 78 | // verify the source is compatible 79 | s, ok := rawS.(compatibleSource) 80 | if !ok { 81 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 82 | } 83 | 84 | allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 85 | if err != nil { 86 | return nil, fmt.Errorf("unable to process parameters: %w", err) 87 | } 88 | 89 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 90 | 91 | // finish tool setup 92 | t := Tool{ 93 | Name: cfg.Name, 94 | Kind: kind, 95 | Parameters: cfg.Parameters, 96 | TemplateParameters: cfg.TemplateParameters, 97 | AllParams: allParameters, 98 | Statement: cfg.Statement, 99 | AuthRequired: cfg.AuthRequired, 100 | Db: s.TrinoDB(), 101 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 102 | mcpManifest: mcpManifest, 103 | } 104 | return t, nil 105 | } 106 | 107 | // validate interface 108 | var _ tools.Tool = Tool{} 109 | 110 | type Tool struct { 111 | Name string `yaml:"name"` 112 | Kind string `yaml:"kind"` 113 | AuthRequired []string `yaml:"authRequired"` 114 | Parameters tools.Parameters `yaml:"parameters"` 115 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 116 | AllParams tools.Parameters `yaml:"allParams"` 117 | 118 | Statement string 119 | Db *sql.DB 120 | manifest tools.Manifest 121 | mcpManifest tools.McpManifest 122 | } 123 | 124 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 125 | paramsMap := params.AsMap() 126 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 127 | if err != nil { 128 | return nil, fmt.Errorf("unable to extract template params %w", err) 129 | } 130 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 131 | if err != nil { 132 | return nil, fmt.Errorf("unable to extract standard params %w", err) 133 | } 134 | sliceParams := newParams.AsSlice() 135 | results, err := t.Db.QueryContext(ctx, newStatement, sliceParams...) 136 | if err != nil { 137 | return nil, fmt.Errorf("unable to execute query: %w", err) 138 | } 139 | defer results.Close() 140 | 141 | cols, err := results.Columns() 142 | if err != nil { 143 | return nil, fmt.Errorf("unable to retrieve column names: %w", err) 144 | } 145 | 146 | // create an array of values for each column, which can be re-used to scan each row 147 | rawValues := make([]any, len(cols)) 148 | values := make([]any, len(cols)) 149 | for i := range rawValues { 150 | values[i] = &rawValues[i] 151 | } 152 | 153 | var out []any 154 | for results.Next() { 155 | err := results.Scan(values...) 156 | if err != nil { 157 | return nil, fmt.Errorf("unable to parse row: %w", err) 158 | } 159 | vMap := make(map[string]any) 160 | for i, name := range cols { 161 | val := rawValues[i] 162 | if val == nil { 163 | vMap[name] = nil 164 | continue 165 | } 166 | 167 | // Convert byte arrays to strings for text fields 168 | if b, ok := val.([]byte); ok { 169 | vMap[name] = string(b) 170 | } else { 171 | vMap[name] = val 172 | } 173 | } 174 | out = append(out, vMap) 175 | } 176 | 177 | if err := results.Err(); err != nil { 178 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 179 | } 180 | 181 | return out, nil 182 | } 183 | 184 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 185 | return tools.ParseParams(t.AllParams, data, claims) 186 | } 187 | 188 | func (t Tool) Manifest() tools.Manifest { 189 | return t.manifest 190 | } 191 | 192 | func (t Tool) McpManifest() tools.McpManifest { 193 | return t.mcpManifest 194 | } 195 | 196 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 197 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 198 | } 199 | 200 | func (t Tool) RequiresClientAuthorization() bool { 201 | return false 202 | } 203 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookeradddashboardelement/lookeradddashboardelement.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 lookeradddashboardelement 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-add-dashboard-element" 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.GetQueryParameters() 76 | 77 | dashIdParameter := tools.NewStringParameter("dashboard_id", "The id of the dashboard where this tile will exist") 78 | parameters = append(parameters, dashIdParameter) 79 | titleParameter := tools.NewStringParameterWithDefault("title", "", "The title of the Dashboard Element") 80 | parameters = append(parameters, titleParameter) 81 | vizParameter := tools.NewMapParameterWithDefault("vis_config", 82 | map[string]any{}, 83 | "The visualization config for the query", 84 | "", 85 | ) 86 | parameters = append(parameters, vizParameter) 87 | 88 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 89 | 90 | // finish tool setup 91 | return Tool{ 92 | Name: cfg.Name, 93 | Kind: kind, 94 | Parameters: parameters, 95 | AuthRequired: cfg.AuthRequired, 96 | UseClientOAuth: s.UseClientOAuth, 97 | Client: s.Client, 98 | ApiSettings: s.ApiSettings, 99 | manifest: tools.Manifest{ 100 | Description: cfg.Description, 101 | Parameters: parameters.Manifest(), 102 | AuthRequired: cfg.AuthRequired, 103 | }, 104 | mcpManifest: mcpManifest, 105 | }, nil 106 | } 107 | 108 | // validate interface 109 | var _ tools.Tool = Tool{} 110 | 111 | type Tool struct { 112 | Name string `yaml:"name"` 113 | Kind string `yaml:"kind"` 114 | UseClientOAuth bool 115 | Client *v4.LookerSDK 116 | ApiSettings *rtl.ApiSettings 117 | AuthRequired []string `yaml:"authRequired"` 118 | Parameters tools.Parameters `yaml:"parameters"` 119 | manifest tools.Manifest 120 | mcpManifest tools.McpManifest 121 | } 122 | 123 | var ( 124 | dataType string = "data" 125 | visType string = "vis" 126 | ) 127 | 128 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 129 | logger, err := util.LoggerFromContext(ctx) 130 | if err != nil { 131 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 132 | } 133 | logger.DebugContext(ctx, "params = ", params) 134 | wq, err := lookercommon.ProcessQueryArgs(ctx, params) 135 | if err != nil { 136 | return nil, fmt.Errorf("error building query request: %w", err) 137 | } 138 | 139 | paramsMap := params.AsMap() 140 | dashboard_id := paramsMap["dashboard_id"].(string) 141 | title := paramsMap["title"].(string) 142 | 143 | visConfig := paramsMap["vis_config"].(map[string]any) 144 | wq.VisConfig = &visConfig 145 | 146 | qrespFields := "id" 147 | 148 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 149 | if err != nil { 150 | return nil, fmt.Errorf("error getting sdk: %w", err) 151 | } 152 | 153 | qresp, err := sdk.CreateQuery(*wq, qrespFields, t.ApiSettings) 154 | if err != nil { 155 | return nil, fmt.Errorf("error making create query request: %w", err) 156 | } 157 | 158 | wde := v4.WriteDashboardElement{ 159 | DashboardId: &dashboard_id, 160 | Title: &title, 161 | QueryId: qresp.Id, 162 | } 163 | switch len(visConfig) { 164 | case 0: 165 | wde.Type = &dataType 166 | default: 167 | wde.Type = &visType 168 | } 169 | 170 | fields := "" 171 | 172 | req := v4.RequestCreateDashboardElement{ 173 | Body: wde, 174 | Fields: &fields, 175 | } 176 | 177 | resp, err := sdk.CreateDashboardElement(req, t.ApiSettings) 178 | if err != nil { 179 | return nil, fmt.Errorf("error making create dashboard element request: %w", err) 180 | } 181 | logger.DebugContext(ctx, "resp = %v", resp) 182 | 183 | data := make(map[string]any) 184 | 185 | data["result"] = fmt.Sprintf("Dashboard element added to dashboard %s", dashboard_id) 186 | 187 | return data, nil 188 | } 189 | 190 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 191 | return tools.ParseParams(t.Parameters, data, claims) 192 | } 193 | 194 | func (t Tool) Manifest() tools.Manifest { 195 | return t.manifest 196 | } 197 | 198 | func (t Tool) McpManifest() tools.McpManifest { 199 | return t.mcpManifest 200 | } 201 | 202 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 203 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 204 | } 205 | 206 | func (t Tool) RequiresClientAuthorization() bool { 207 | return t.UseClientOAuth 208 | } 209 | ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbaggregate/mongodbaggregate.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 mongodbaggregate 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "fmt" 20 | "slices" 21 | 22 | "github.com/goccy/go-yaml" 23 | mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/mongo" 26 | 27 | "github.com/googleapis/genai-toolbox/internal/sources" 28 | "github.com/googleapis/genai-toolbox/internal/tools" 29 | ) 30 | 31 | const kind string = "mongodb-aggregate" 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 | AuthRequired []string `yaml:"authRequired" validate:"required"` 52 | Description string `yaml:"description" validate:"required"` 53 | Database string `yaml:"database" validate:"required"` 54 | Collection string `yaml:"collection" validate:"required"` 55 | PipelinePayload string `yaml:"pipelinePayload" validate:"required"` 56 | PipelineParams tools.Parameters `yaml:"pipelineParams" validate:"required"` 57 | Canonical bool `yaml:"canonical"` 58 | ReadOnly bool `yaml:"readOnly"` 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.(*mongosrc.Source) 77 | if !ok { 78 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind) 79 | } 80 | 81 | // Create a slice for all parameters 82 | allParameters := slices.Concat(cfg.PipelineParams) 83 | 84 | // Create Toolbox manifest 85 | paramManifest := allParameters.Manifest() 86 | 87 | if paramManifest == nil { 88 | paramManifest = make([]tools.ParameterManifest, 0) 89 | } 90 | 91 | // Create MCP manifest 92 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 93 | 94 | // finish tool setup 95 | return Tool{ 96 | Name: cfg.Name, 97 | Kind: kind, 98 | AuthRequired: cfg.AuthRequired, 99 | Collection: cfg.Collection, 100 | PipelinePayload: cfg.PipelinePayload, 101 | PipelineParams: cfg.PipelineParams, 102 | Canonical: cfg.Canonical, 103 | ReadOnly: cfg.ReadOnly, 104 | AllParams: allParameters, 105 | database: s.Client.Database(cfg.Database), 106 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 107 | mcpManifest: mcpManifest, 108 | }, nil 109 | } 110 | 111 | // validate interface 112 | var _ tools.Tool = Tool{} 113 | 114 | type Tool struct { 115 | Name string `yaml:"name"` 116 | Kind string `yaml:"kind"` 117 | Description string `yaml:"description"` 118 | AuthRequired []string `yaml:"authRequired"` 119 | Collection string `yaml:"collection"` 120 | PipelinePayload string `yaml:"pipelinePayload"` 121 | PipelineParams tools.Parameters `yaml:"pipelineParams"` 122 | Canonical bool `yaml:"canonical"` 123 | ReadOnly bool `yaml:"readOnly"` 124 | AllParams tools.Parameters `yaml:"allParams"` 125 | 126 | database *mongo.Database 127 | manifest tools.Manifest 128 | mcpManifest tools.McpManifest 129 | } 130 | 131 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 132 | paramsMap := params.AsMap() 133 | 134 | pipelineString, err := tools.PopulateTemplateWithJSON("MongoDBAggregatePipeline", t.PipelinePayload, paramsMap) 135 | if err != nil { 136 | return nil, fmt.Errorf("error populating pipeline: %s", err) 137 | } 138 | 139 | var pipeline = []bson.M{} 140 | err = bson.UnmarshalExtJSON([]byte(pipelineString), t.Canonical, &pipeline) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | if t.ReadOnly { 146 | //fail if we do a merge or an out 147 | for _, stage := range pipeline { 148 | for key := range stage { 149 | if key == "$merge" || key == "$out" { 150 | return nil, fmt.Errorf("this is not a read-only pipeline: %+v", stage) 151 | } 152 | } 153 | } 154 | } 155 | 156 | cur, err := t.database.Collection(t.Collection).Aggregate(ctx, pipeline) 157 | if err != nil { 158 | return nil, err 159 | } 160 | defer cur.Close(ctx) 161 | 162 | var data = []any{} 163 | err = cur.All(ctx, &data) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if len(data) == 0 { 169 | return []any{}, nil 170 | } 171 | 172 | var final []any 173 | for _, item := range data { 174 | tmp, _ := bson.MarshalExtJSON(item, false, false) 175 | var tmp2 any 176 | err = json.Unmarshal(tmp, &tmp2) 177 | if err != nil { 178 | return nil, err 179 | } 180 | final = append(final, tmp2) 181 | } 182 | 183 | return final, err 184 | } 185 | 186 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 187 | return tools.ParseParams(t.AllParams, data, claims) 188 | } 189 | 190 | func (t Tool) Manifest() tools.Manifest { 191 | return t.manifest 192 | } 193 | 194 | func (t Tool) McpManifest() tools.McpManifest { 195 | return t.mcpManifest 196 | } 197 | 198 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 199 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 200 | } 201 | 202 | func (t Tool) RequiresClientAuthorization() bool { 203 | return false 204 | } 205 | ``` -------------------------------------------------------------------------------- /internal/tools/http/http_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 http_test 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/googleapis/genai-toolbox/internal/server" 24 | "github.com/googleapis/genai-toolbox/internal/testutils" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | http "github.com/googleapis/genai-toolbox/internal/tools/http" 27 | ) 28 | 29 | func TestParseFromYamlHTTP(t *testing.T) { 30 | ctx, err := testutils.ContextWithNewLogger() 31 | if err != nil { 32 | t.Fatalf("unexpected error: %s", err) 33 | } 34 | tcs := []struct { 35 | desc string 36 | in string 37 | want server.ToolConfigs 38 | }{ 39 | { 40 | desc: "basic example", 41 | in: ` 42 | tools: 43 | example_tool: 44 | kind: http 45 | source: my-instance 46 | method: GET 47 | description: some description 48 | path: search 49 | `, 50 | want: server.ToolConfigs{ 51 | "example_tool": http.Config{ 52 | Name: "example_tool", 53 | Kind: "http", 54 | Source: "my-instance", 55 | Method: "GET", 56 | Path: "search", 57 | Description: "some description", 58 | AuthRequired: []string{}, 59 | }, 60 | }, 61 | }, 62 | { 63 | desc: "advanced example", 64 | in: ` 65 | tools: 66 | example_tool: 67 | kind: http 68 | source: my-instance 69 | method: GET 70 | path: "{{.pathParam}}?name=alice&pet=cat" 71 | description: some description 72 | authRequired: 73 | - my-google-auth-service 74 | - other-auth-service 75 | queryParams: 76 | - name: country 77 | type: string 78 | description: some description 79 | authServices: 80 | - name: my-google-auth-service 81 | field: user_id 82 | - name: other-auth-service 83 | field: user_id 84 | pathParams: 85 | - name: pathParam 86 | type: string 87 | description: path param 88 | requestBody: | 89 | { 90 | "age": {{.age}}, 91 | "city": "{{.city}}", 92 | "food": {{.food}} 93 | } 94 | bodyParams: 95 | - name: age 96 | type: integer 97 | description: age num 98 | - name: city 99 | type: string 100 | description: city string 101 | headers: 102 | Authorization: API_KEY 103 | Content-Type: application/json 104 | headerParams: 105 | - name: Language 106 | type: string 107 | description: language string 108 | `, 109 | want: server.ToolConfigs{ 110 | "example_tool": http.Config{ 111 | Name: "example_tool", 112 | Kind: "http", 113 | Source: "my-instance", 114 | Method: "GET", 115 | Path: "{{.pathParam}}?name=alice&pet=cat", 116 | Description: "some description", 117 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 118 | QueryParams: []tools.Parameter{ 119 | tools.NewStringParameterWithAuth("country", "some description", 120 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 121 | {Name: "other-auth-service", Field: "user_id"}}), 122 | }, 123 | PathParams: tools.Parameters{ 124 | &tools.StringParameter{ 125 | CommonParameter: tools.CommonParameter{Name: "pathParam", Type: "string", Desc: "path param"}, 126 | }, 127 | }, 128 | RequestBody: `{ 129 | "age": {{.age}}, 130 | "city": "{{.city}}", 131 | "food": {{.food}} 132 | } 133 | `, 134 | BodyParams: []tools.Parameter{tools.NewIntParameter("age", "age num"), tools.NewStringParameter("city", "city string")}, 135 | Headers: map[string]string{"Authorization": "API_KEY", "Content-Type": "application/json"}, 136 | HeaderParams: []tools.Parameter{tools.NewStringParameter("Language", "language string")}, 137 | }, 138 | }, 139 | }, 140 | } 141 | for _, tc := range tcs { 142 | t.Run(tc.desc, func(t *testing.T) { 143 | got := struct { 144 | Tools server.ToolConfigs `yaml:"tools"` 145 | }{} 146 | // Parse contents 147 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 148 | if err != nil { 149 | t.Fatalf("unable to unmarshal: %s", err) 150 | } 151 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 152 | t.Fatalf("incorrect parse: diff %v", diff) 153 | } 154 | }) 155 | } 156 | 157 | } 158 | 159 | func TestFailParseFromYamlHTTP(t *testing.T) { 160 | ctx, err := testutils.ContextWithNewLogger() 161 | if err != nil { 162 | t.Fatalf("unexpected error: %s", err) 163 | } 164 | tcs := []struct { 165 | desc string 166 | in string 167 | err string 168 | }{ 169 | { 170 | desc: "Invalid method", 171 | in: ` 172 | tools: 173 | example_tool: 174 | kind: http 175 | source: my-instance 176 | method: GOT 177 | path: "search?name=alice&pet=cat" 178 | description: some description 179 | authRequired: 180 | - my-google-auth-service 181 | - other-auth-service 182 | queryParams: 183 | - name: country 184 | type: string 185 | description: some description 186 | authServices: 187 | - name: my-google-auth-service 188 | field: user_id 189 | - name: other-auth-service 190 | field: user_id 191 | requestBody: | 192 | { 193 | "age": {{.age}}, 194 | "city": "{{.city}}" 195 | } 196 | bodyParams: 197 | - name: age 198 | type: integer 199 | description: age num 200 | - name: city 201 | type: string 202 | description: city string 203 | headers: 204 | Authorization: API_KEY 205 | Content-Type: application/json 206 | headerParams: 207 | - name: Language 208 | type: string 209 | description: language string 210 | `, 211 | err: `GOT is not a valid http method`, 212 | }, 213 | } 214 | for _, tc := range tcs { 215 | t.Run(tc.desc, func(t *testing.T) { 216 | got := struct { 217 | Tools server.ToolConfigs `yaml:"tools"` 218 | }{} 219 | // Parse contents 220 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 221 | if err == nil { 222 | t.Fatalf("expect parsing to fail") 223 | } 224 | errStr := err.Error() 225 | if !strings.Contains(errStr, tc.err) { 226 | t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) 227 | } 228 | }) 229 | } 230 | 231 | } 232 | ``` -------------------------------------------------------------------------------- /internal/tools/firestore/firestoregetdocuments/firestoregetdocuments.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 firestoregetdocuments 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | firestoreapi "cloud.google.com/go/firestore" 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/tools/firestore/util" 27 | ) 28 | 29 | const kind string = "firestore-get-documents" 30 | const documentPathsKey string = "documentPaths" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | FirestoreClient() *firestoreapi.Client 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &firestoreds.Source{} 52 | 53 | var compatibleSources = [...]string{firestoreds.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" validate:"required"` 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 | // verify source exists 72 | rawS, ok := srcs[cfg.Source] 73 | if !ok { 74 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 75 | } 76 | 77 | // verify the source is compatible 78 | s, ok := rawS.(compatibleSource) 79 | if !ok { 80 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 81 | } 82 | 83 | documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to retrieve from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path")) 84 | parameters := tools.Parameters{documentPathsParameter} 85 | 86 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 87 | 88 | // finish tool setup 89 | t := Tool{ 90 | Name: cfg.Name, 91 | Kind: kind, 92 | Parameters: parameters, 93 | AuthRequired: cfg.AuthRequired, 94 | Client: s.FirestoreClient(), 95 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 96 | mcpManifest: mcpManifest, 97 | } 98 | return t, nil 99 | } 100 | 101 | // validate interface 102 | var _ tools.Tool = Tool{} 103 | 104 | type Tool struct { 105 | Name string `yaml:"name"` 106 | Kind string `yaml:"kind"` 107 | AuthRequired []string `yaml:"authRequired"` 108 | Parameters tools.Parameters `yaml:"parameters"` 109 | 110 | Client *firestoreapi.Client 111 | manifest tools.Manifest 112 | mcpManifest tools.McpManifest 113 | } 114 | 115 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 116 | mapParams := params.AsMap() 117 | documentPathsRaw, ok := mapParams[documentPathsKey].([]any) 118 | if !ok { 119 | return nil, fmt.Errorf("invalid or missing '%s' parameter; expected an array", documentPathsKey) 120 | } 121 | 122 | if len(documentPathsRaw) == 0 { 123 | return nil, fmt.Errorf("'%s' parameter cannot be empty", documentPathsKey) 124 | } 125 | 126 | // Use ConvertAnySliceToTyped to convert the slice 127 | typedSlice, err := tools.ConvertAnySliceToTyped(documentPathsRaw, "string") 128 | if err != nil { 129 | return nil, fmt.Errorf("failed to convert document paths: %w", err) 130 | } 131 | 132 | documentPaths, ok := typedSlice.([]string) 133 | if !ok { 134 | return nil, fmt.Errorf("unexpected type conversion error for document paths") 135 | } 136 | 137 | // Validate each document path 138 | for i, path := range documentPaths { 139 | if err := util.ValidateDocumentPath(path); err != nil { 140 | return nil, fmt.Errorf("invalid document path at index %d: %w", i, err) 141 | } 142 | } 143 | 144 | // Create document references from paths 145 | docRefs := make([]*firestoreapi.DocumentRef, len(documentPaths)) 146 | for i, path := range documentPaths { 147 | docRefs[i] = t.Client.Doc(path) 148 | } 149 | 150 | // Get all documents 151 | snapshots, err := t.Client.GetAll(ctx, docRefs) 152 | if err != nil { 153 | return nil, fmt.Errorf("failed to get documents: %w", err) 154 | } 155 | 156 | // Convert snapshots to response data 157 | results := make([]any, len(snapshots)) 158 | for i, snapshot := range snapshots { 159 | docData := make(map[string]any) 160 | docData["path"] = documentPaths[i] 161 | docData["exists"] = snapshot.Exists() 162 | 163 | if snapshot.Exists() { 164 | docData["data"] = snapshot.Data() 165 | docData["createTime"] = snapshot.CreateTime 166 | docData["updateTime"] = snapshot.UpdateTime 167 | docData["readTime"] = snapshot.ReadTime 168 | } 169 | 170 | results[i] = docData 171 | } 172 | 173 | return results, nil 174 | } 175 | 176 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 177 | return tools.ParseParams(t.Parameters, data, claims) 178 | } 179 | 180 | func (t Tool) Manifest() tools.Manifest { 181 | return t.manifest 182 | } 183 | 184 | func (t Tool) McpManifest() tools.McpManifest { 185 | return t.mcpManifest 186 | } 187 | 188 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 189 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 190 | } 191 | 192 | func (t Tool) RequiresClientAuthorization() bool { 193 | return false 194 | } 195 | ```