This is page 16 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/lookerqueryurl/lookerqueryurl.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 lookerqueryurl 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-query-url" 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 | vizParameter := tools.NewMapParameterWithDefault("vis_config", 78 | map[string]any{}, 79 | "The visualization config for the query", 80 | "", 81 | ) 82 | parameters = append(parameters, vizParameter) 83 | 84 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 85 | 86 | // finish tool setup 87 | return Tool{ 88 | Name: cfg.Name, 89 | Kind: kind, 90 | Parameters: parameters, 91 | AuthRequired: cfg.AuthRequired, 92 | UseClientOAuth: s.UseClientOAuth, 93 | Client: s.Client, 94 | ApiSettings: s.ApiSettings, 95 | manifest: tools.Manifest{ 96 | Description: cfg.Description, 97 | Parameters: parameters.Manifest(), 98 | AuthRequired: cfg.AuthRequired, 99 | }, 100 | mcpManifest: mcpManifest, 101 | }, nil 102 | } 103 | 104 | // validate interface 105 | var _ tools.Tool = Tool{} 106 | 107 | type Tool struct { 108 | Name string `yaml:"name"` 109 | Kind string `yaml:"kind"` 110 | UseClientOAuth bool 111 | Client *v4.LookerSDK 112 | ApiSettings *rtl.ApiSettings 113 | AuthRequired []string `yaml:"authRequired"` 114 | Parameters tools.Parameters `yaml:"parameters"` 115 | manifest tools.Manifest 116 | mcpManifest tools.McpManifest 117 | } 118 | 119 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 120 | logger, err := util.LoggerFromContext(ctx) 121 | if err != nil { 122 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 123 | } 124 | logger.DebugContext(ctx, "params = ", params) 125 | wq, err := lookercommon.ProcessQueryArgs(ctx, params) 126 | if err != nil { 127 | return nil, fmt.Errorf("error building query request: %w", err) 128 | } 129 | 130 | paramsMap := params.AsMap() 131 | visConfig := paramsMap["vis_config"].(map[string]any) 132 | wq.VisConfig = &visConfig 133 | 134 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 135 | if err != nil { 136 | return nil, fmt.Errorf("error getting sdk: %w", err) 137 | } 138 | respFields := "id,slug,share_url,expanded_share_url" 139 | resp, err := sdk.CreateQuery(*wq, respFields, t.ApiSettings) 140 | if err != nil { 141 | return nil, fmt.Errorf("error making query request: %s", err) 142 | } 143 | logger.DebugContext(ctx, "resp = ", resp) 144 | 145 | data := make(map[string]any) 146 | if resp.Id != nil { 147 | data["id"] = *resp.Id 148 | } 149 | if resp.Slug != nil { 150 | data["slug"] = *resp.Slug 151 | } 152 | if resp.ShareUrl != nil { 153 | data["url"] = *resp.ShareUrl 154 | } 155 | if resp.ExpandedShareUrl != nil { 156 | data["long_url"] = *resp.ExpandedShareUrl 157 | } 158 | logger.DebugContext(ctx, "data = %v", data) 159 | 160 | return data, nil 161 | } 162 | 163 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 164 | return tools.ParseParams(t.Parameters, data, claims) 165 | } 166 | 167 | func (t Tool) Manifest() tools.Manifest { 168 | return t.manifest 169 | } 170 | 171 | func (t Tool) McpManifest() tools.McpManifest { 172 | return t.mcpManifest 173 | } 174 | 175 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 176 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 177 | } 178 | 179 | func (t Tool) RequiresClientAuthorization() bool { 180 | return t.UseClientOAuth 181 | } 182 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetprojectfiles/lookergetprojectfiles.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 lookergetprojectfiles 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-project-files" 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 | projectIdParameter := tools.NewStringParameter("project_id", "The id of the project containing the files") 76 | parameters := tools.Parameters{projectIdParameter} 77 | 78 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 79 | 80 | // finish tool setup 81 | return Tool{ 82 | Name: cfg.Name, 83 | Kind: kind, 84 | Parameters: parameters, 85 | AuthRequired: cfg.AuthRequired, 86 | UseClientOAuth: s.UseClientOAuth, 87 | Client: s.Client, 88 | ApiSettings: s.ApiSettings, 89 | manifest: tools.Manifest{ 90 | Description: cfg.Description, 91 | Parameters: parameters.Manifest(), 92 | AuthRequired: cfg.AuthRequired, 93 | }, 94 | mcpManifest: mcpManifest, 95 | }, nil 96 | } 97 | 98 | // validate interface 99 | var _ tools.Tool = Tool{} 100 | 101 | type Tool struct { 102 | Name string `yaml:"name"` 103 | Kind string `yaml:"kind"` 104 | UseClientOAuth bool 105 | Client *v4.LookerSDK 106 | ApiSettings *rtl.ApiSettings 107 | AuthRequired []string `yaml:"authRequired"` 108 | Parameters tools.Parameters `yaml:"parameters"` 109 | manifest tools.Manifest 110 | mcpManifest tools.McpManifest 111 | } 112 | 113 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 114 | logger, err := util.LoggerFromContext(ctx) 115 | if err != nil { 116 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 117 | } 118 | 119 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 120 | if err != nil { 121 | return nil, fmt.Errorf("error getting sdk: %w", err) 122 | } 123 | 124 | mapParams := params.AsMap() 125 | projectId, ok := mapParams["project_id"].(string) 126 | if !ok { 127 | return nil, fmt.Errorf("'project_id' must be a string, got %T", mapParams["project_id"]) 128 | } 129 | 130 | resp, err := sdk.AllProjectFiles(projectId, "", t.ApiSettings) 131 | if err != nil { 132 | return nil, fmt.Errorf("error making get_project_files request: %s", err) 133 | } 134 | 135 | var data []any 136 | for _, v := range resp { 137 | logger.DebugContext(ctx, "Got response element of %v\n", v) 138 | vMap := make(map[string]any) 139 | if v.Id != nil { 140 | vMap["id"] = *v.Id 141 | } 142 | if v.Path != nil { 143 | vMap["path"] = *v.Path 144 | } 145 | if v.Title != nil { 146 | vMap["title"] = *v.Title 147 | } 148 | if v.Type != nil { 149 | vMap["type"] = *v.Type 150 | } 151 | if v.Extension != nil { 152 | vMap["extension"] = *v.Extension 153 | } 154 | if v.Editable != nil { 155 | vMap["editable"] = *v.Editable 156 | } 157 | logger.DebugContext(ctx, "Converted to %v\n", vMap) 158 | data = append(data, vMap) 159 | } 160 | logger.DebugContext(ctx, "data = ", data) 161 | 162 | return data, nil 163 | } 164 | 165 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 166 | return tools.ParseParams(t.Parameters, data, claims) 167 | } 168 | 169 | func (t Tool) Manifest() tools.Manifest { 170 | return t.manifest 171 | } 172 | 173 | func (t Tool) McpManifest() tools.McpManifest { 174 | return t.mcpManifest 175 | } 176 | 177 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 178 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 179 | } 180 | 181 | func (t Tool) RequiresClientAuthorization() bool { 182 | return t.UseClientOAuth 183 | } 184 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookercreateprojectfile/lookercreateprojectfile.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 lookercreateprojectfile 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 | 26 | "github.com/looker-open-source/sdk-codegen/go/rtl" 27 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 28 | ) 29 | 30 | const kind string = "looker-create-project-file" 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 Config struct { 47 | Name string `yaml:"name" validate:"required"` 48 | Kind string `yaml:"kind" validate:"required"` 49 | Source string `yaml:"source" validate:"required"` 50 | Description string `yaml:"description" validate:"required"` 51 | AuthRequired []string `yaml:"authRequired"` 52 | } 53 | 54 | // validate interface 55 | var _ tools.ToolConfig = Config{} 56 | 57 | func (cfg Config) ToolConfigKind() string { 58 | return kind 59 | } 60 | 61 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 62 | // verify source exists 63 | rawS, ok := srcs[cfg.Source] 64 | if !ok { 65 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 66 | } 67 | 68 | // verify the source is compatible 69 | s, ok := rawS.(*lookersrc.Source) 70 | if !ok { 71 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 72 | } 73 | 74 | projectIdParameter := tools.NewStringParameter("project_id", "The id of the project containing the files") 75 | filePathParameter := tools.NewStringParameter("file_path", "The path of the file within the project") 76 | fileContentParameter := tools.NewStringParameter("file_content", "The content of the file") 77 | parameters := tools.Parameters{projectIdParameter, filePathParameter, fileContentParameter} 78 | 79 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 80 | 81 | // finish tool setup 82 | return Tool{ 83 | Name: cfg.Name, 84 | Kind: kind, 85 | Parameters: parameters, 86 | AuthRequired: cfg.AuthRequired, 87 | UseClientOAuth: s.UseClientOAuth, 88 | Client: s.Client, 89 | ApiSettings: s.ApiSettings, 90 | manifest: tools.Manifest{ 91 | Description: cfg.Description, 92 | Parameters: parameters.Manifest(), 93 | AuthRequired: cfg.AuthRequired, 94 | }, 95 | mcpManifest: mcpManifest, 96 | }, nil 97 | } 98 | 99 | // validate interface 100 | var _ tools.Tool = Tool{} 101 | 102 | type Tool struct { 103 | Name string `yaml:"name"` 104 | Kind string `yaml:"kind"` 105 | UseClientOAuth bool 106 | Client *v4.LookerSDK 107 | ApiSettings *rtl.ApiSettings 108 | AuthRequired []string `yaml:"authRequired"` 109 | Parameters tools.Parameters `yaml:"parameters"` 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 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 116 | if err != nil { 117 | return nil, fmt.Errorf("error getting sdk: %w", err) 118 | } 119 | 120 | mapParams := params.AsMap() 121 | projectId, ok := mapParams["project_id"].(string) 122 | if !ok { 123 | return nil, fmt.Errorf("'project_id' must be a string, got %T", mapParams["project_id"]) 124 | } 125 | filePath, ok := mapParams["file_path"].(string) 126 | if !ok { 127 | return nil, fmt.Errorf("'file_path' must be a string, got %T", mapParams["file_path"]) 128 | } 129 | fileContent, ok := mapParams["file_content"].(string) 130 | if !ok { 131 | return nil, fmt.Errorf("'file_content' must be a string, got %T", mapParams["file_content"]) 132 | } 133 | 134 | req := lookercommon.FileContent{ 135 | Path: filePath, 136 | Content: fileContent, 137 | } 138 | 139 | err = lookercommon.CreateProjectFile(sdk, projectId, req, t.ApiSettings) 140 | if err != nil { 141 | return nil, fmt.Errorf("error making create_project_file request: %s", err) 142 | } 143 | 144 | data := make(map[string]any) 145 | data["type"] = "text" 146 | data["text"] = fmt.Sprintf("created file %s in project %s", filePath, projectId) 147 | 148 | return data, nil 149 | } 150 | 151 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 152 | return tools.ParseParams(t.Parameters, data, claims) 153 | } 154 | 155 | func (t Tool) Manifest() tools.Manifest { 156 | return t.manifest 157 | } 158 | 159 | func (t Tool) McpManifest() tools.McpManifest { 160 | return t.mcpManifest 161 | } 162 | 163 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 164 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 165 | } 166 | 167 | func (t Tool) RequiresClientAuthorization() bool { 168 | return t.UseClientOAuth 169 | } ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookerupdateprojectfile/lookerupdateprojectfile.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 lookerupdateprojectfile 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 | 26 | "github.com/looker-open-source/sdk-codegen/go/rtl" 27 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 28 | ) 29 | 30 | const kind string = "looker-update-project-file" 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 Config struct { 47 | Name string `yaml:"name" validate:"required"` 48 | Kind string `yaml:"kind" validate:"required"` 49 | Source string `yaml:"source" validate:"required"` 50 | Description string `yaml:"description" validate:"required"` 51 | AuthRequired []string `yaml:"authRequired"` 52 | } 53 | 54 | // validate interface 55 | var _ tools.ToolConfig = Config{} 56 | 57 | func (cfg Config) ToolConfigKind() string { 58 | return kind 59 | } 60 | 61 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 62 | // verify source exists 63 | rawS, ok := srcs[cfg.Source] 64 | if !ok { 65 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 66 | } 67 | 68 | // verify the source is compatible 69 | s, ok := rawS.(*lookersrc.Source) 70 | if !ok { 71 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) 72 | } 73 | 74 | projectIdParameter := tools.NewStringParameter("project_id", "The id of the project containing the files") 75 | filePathParameter := tools.NewStringParameter("file_path", "The path of the file within the project") 76 | fileContentParameter := tools.NewStringParameter("file_content", "The content of the file") 77 | parameters := tools.Parameters{projectIdParameter, filePathParameter, fileContentParameter} 78 | 79 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 80 | 81 | // finish tool setup 82 | return Tool{ 83 | Name: cfg.Name, 84 | Kind: kind, 85 | Parameters: parameters, 86 | AuthRequired: cfg.AuthRequired, 87 | UseClientOAuth: s.UseClientOAuth, 88 | Client: s.Client, 89 | ApiSettings: s.ApiSettings, 90 | manifest: tools.Manifest{ 91 | Description: cfg.Description, 92 | Parameters: parameters.Manifest(), 93 | AuthRequired: cfg.AuthRequired, 94 | }, 95 | mcpManifest: mcpManifest, 96 | }, nil 97 | } 98 | 99 | // validate interface 100 | var _ tools.Tool = Tool{} 101 | 102 | type Tool struct { 103 | Name string `yaml:"name"` 104 | Kind string `yaml:"kind"` 105 | UseClientOAuth bool 106 | Client *v4.LookerSDK 107 | ApiSettings *rtl.ApiSettings 108 | AuthRequired []string `yaml:"authRequired"` 109 | Parameters tools.Parameters `yaml:"parameters"` 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 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 116 | if err != nil { 117 | return nil, fmt.Errorf("error getting sdk: %w", err) 118 | } 119 | 120 | mapParams := params.AsMap() 121 | projectId, ok := mapParams["project_id"].(string) 122 | if !ok { 123 | return nil, fmt.Errorf("'project_id' must be a string, got %T", mapParams["project_id"]) 124 | } 125 | filePath, ok := mapParams["file_path"].(string) 126 | if !ok { 127 | return nil, fmt.Errorf("'file_path' must be a string, got %T", mapParams["file_path"]) 128 | } 129 | fileContent, ok := mapParams["file_content"].(string) 130 | if !ok { 131 | return nil, fmt.Errorf("'file_content' must be a string, got %T", mapParams["file_content"]) 132 | } 133 | 134 | req := lookercommon.FileContent{ 135 | Path: filePath, 136 | Content: fileContent, 137 | } 138 | 139 | err = lookercommon.UpdateProjectFile(sdk, projectId, req, t.ApiSettings) 140 | if err != nil { 141 | return nil, fmt.Errorf("error making update_project_file request: %s", err) 142 | } 143 | 144 | data := make(map[string]any) 145 | data["type"] = "text" 146 | data["text"] = fmt.Sprintf("updated file %s in project %s", filePath, projectId) 147 | 148 | return data, nil 149 | } 150 | 151 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 152 | return tools.ParseParams(t.Parameters, data, claims) 153 | } 154 | 155 | func (t Tool) Manifest() tools.Manifest { 156 | return t.manifest 157 | } 158 | 159 | func (t Tool) McpManifest() tools.McpManifest { 160 | return t.mcpManifest 161 | } 162 | 163 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 164 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 165 | } 166 | 167 | func (t Tool) RequiresClientAuthorization() bool { 168 | return t.UseClientOAuth 169 | } ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/firestore/firestore-query-collection.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "firestore-query-collection" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "firestore-query-collection" tool allow to query collections in Firestore. 7 | aliases: 8 | - /resources/tools/firestore-query-collection 9 | --- 10 | 11 | ## About 12 | 13 | The `firestore-query-collection` tool allows you to query Firestore collections 14 | with filters, ordering, and limit capabilities. 15 | 16 | ## Configuration 17 | 18 | To use this tool, you need to configure it in your YAML configuration file: 19 | 20 | ```yaml 21 | sources: 22 | my-firestore: 23 | kind: firestore 24 | project: my-gcp-project 25 | database: "(default)" 26 | 27 | tools: 28 | query_collection: 29 | kind: firestore-query-collection 30 | source: my-firestore 31 | description: Query Firestore collections with advanced filtering 32 | ``` 33 | 34 | ## Parameters 35 | 36 | | **parameters** | **type** | **required** | **default** | **description** | 37 | |------------------|:------------:|:------------:|:-----------:|-----------------------------------------------------------------------| 38 | | `collectionPath` | string | true | - | The Firestore Rules source code to validate | 39 | | `filters` | array | false | - | Array of filter objects (as JSON strings) to apply to the query | 40 | | `orderBy` | string | false | - | JSON string specifying field and direction to order results | 41 | | `limit` | integer | false | 100 | Maximum number of documents to return | 42 | | `analyzeQuery` | boolean | false | false | If true, returns query explain metrics including execution statistics | 43 | 44 | ### Filter Format 45 | 46 | Each filter in the `filters` array should be a JSON string with the following 47 | structure: 48 | 49 | ```json 50 | { 51 | "field": "fieldName", 52 | "op": "operator", 53 | "value": "compareValue" 54 | } 55 | ``` 56 | 57 | Supported operators: 58 | 59 | - `<` - Less than 60 | - `<=` - Less than or equal to 61 | - `>` - Greater than 62 | - `>=` - Greater than or equal to 63 | - `==` - Equal to 64 | - `!=` - Not equal to 65 | - `array-contains` - Array contains a specific value 66 | - `array-contains-any` - Array contains any of the specified values 67 | - `in` - Field value is in the specified array 68 | - `not-in` - Field value is not in the specified array 69 | 70 | Value types supported: 71 | 72 | - String: `"value": "text"` 73 | - Number: `"value": 123` or `"value": 45.67` 74 | - Boolean: `"value": true` or `"value": false` 75 | - Array: `"value": ["item1", "item2"]` (for `in`, `not-in`, `array-contains-any` 76 | operators) 77 | 78 | ### OrderBy Format 79 | 80 | The `orderBy` parameter should be a JSON string with the following structure: 81 | 82 | ```json 83 | { 84 | "field": "fieldName", 85 | "direction": "ASCENDING" 86 | } 87 | ``` 88 | 89 | Direction values: 90 | 91 | - `ASCENDING` 92 | - `DESCENDING` 93 | 94 | ## Example Usage 95 | 96 | ### Query with filters 97 | 98 | ```json 99 | { 100 | "collectionPath": "users", 101 | "filters": [ 102 | "{\"field\": \"age\", \"op\": \">\", \"value\": 18}", 103 | "{\"field\": \"status\", \"op\": \"==\", \"value\": \"active\"}" 104 | ], 105 | "orderBy": "{\"field\": \"createdAt\", \"direction\": \"DESCENDING\"}", 106 | "limit": 50 107 | } 108 | ``` 109 | 110 | ### Query with array contains filter 111 | 112 | ```json 113 | { 114 | "collectionPath": "products", 115 | "filters": [ 116 | "{\"field\": \"categories\", \"op\": \"array-contains\", \"value\": \"electronics\"}", 117 | "{\"field\": \"price\", \"op\": \"<\", \"value\": 1000}" 118 | ], 119 | "orderBy": "{\"field\": \"price\", \"direction\": \"ASCENDING\"}", 120 | "limit": 20 121 | } 122 | ``` 123 | 124 | ### Query with IN operator 125 | 126 | ```json 127 | { 128 | "collectionPath": "orders", 129 | "filters": [ 130 | "{\"field\": \"status\", \"op\": \"in\", \"value\": [\"pending\", \"processing\"]}" 131 | ], 132 | "limit": 100 133 | } 134 | ``` 135 | 136 | ### Query with explain metrics 137 | 138 | ```json 139 | { 140 | "collectionPath": "users", 141 | "filters": [ 142 | "{\"field\": \"age\", \"op\": \">=\", \"value\": 21}", 143 | "{\"field\": \"active\", \"op\": \"==\", \"value\": true}" 144 | ], 145 | "orderBy": "{\"field\": \"lastLogin\", \"direction\": \"DESCENDING\"}", 146 | "limit": 25, 147 | "analyzeQuery": true 148 | } 149 | ``` 150 | 151 | ## Response Format 152 | 153 | ### Standard Response (analyzeQuery = false) 154 | 155 | The tool returns an array of documents, where each document includes: 156 | 157 | ```json 158 | { 159 | "id": "documentId", 160 | "path": "collection/documentId", 161 | "data": { 162 | // Document fields 163 | }, 164 | "createTime": "2025-01-07T12:00:00Z", 165 | "updateTime": "2025-01-07T12:00:00Z", 166 | "readTime": "2025-01-07T12:00:00Z" 167 | } 168 | ``` 169 | 170 | ### Response with Query Analysis (analyzeQuery = true) 171 | 172 | When `analyzeQuery` is set to true, the tool returns a single object containing 173 | documents and explain metrics: 174 | 175 | ```json 176 | { 177 | "documents": [ 178 | // Array of document objects as shown above 179 | ], 180 | "explainMetrics": { 181 | "planSummary": { 182 | "indexesUsed": [ 183 | { 184 | "query_scope": "Collection", 185 | "properties": "(field ASC, __name__ ASC)" 186 | } 187 | ] 188 | }, 189 | "executionStats": { 190 | "resultsReturned": 50, 191 | "readOperations": 50, 192 | "executionDuration": "120ms", 193 | "debugStats": { 194 | "indexes_entries_scanned": "1000", 195 | "documents_scanned": "50", 196 | "billing_details": { 197 | "documents_billable": "50", 198 | "index_entries_billable": "1000", 199 | "min_query_cost": "0" 200 | } 201 | } 202 | } 203 | } 204 | } 205 | ``` 206 | 207 | ## Error Handling 208 | 209 | The tool will return errors for: 210 | 211 | - Invalid collection path 212 | - Malformed filter JSON 213 | - Unsupported operators 214 | - Query execution failures 215 | - Invalid orderBy format 216 | ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.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 mongodbdeleteone 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "slices" 20 | 21 | "github.com/goccy/go-yaml" 22 | mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb" 23 | "go.mongodb.org/mongo-driver/bson" 24 | "go.mongodb.org/mongo-driver/mongo" 25 | "go.mongodb.org/mongo-driver/mongo/options" 26 | 27 | "github.com/googleapis/genai-toolbox/internal/sources" 28 | "github.com/googleapis/genai-toolbox/internal/tools" 29 | ) 30 | 31 | const kind string = "mongodb-delete-one" 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 | FilterPayload string `yaml:"filterPayload" validate:"required"` 56 | FilterParams tools.Parameters `yaml:"filterParams" validate:"required"` 57 | } 58 | 59 | // validate interface 60 | var _ tools.ToolConfig = Config{} 61 | 62 | func (cfg Config) ToolConfigKind() string { 63 | return kind 64 | } 65 | 66 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 67 | // verify source exists 68 | rawS, ok := srcs[cfg.Source] 69 | if !ok { 70 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 71 | } 72 | 73 | // verify the source is compatible 74 | s, ok := rawS.(*mongosrc.Source) 75 | if !ok { 76 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind) 77 | } 78 | 79 | // Create a slice for all parameters 80 | allParameters := slices.Concat(cfg.FilterParams) 81 | 82 | // Verify no duplicate parameter names 83 | err := tools.CheckDuplicateParameters(allParameters) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | // Create Toolbox manifest 89 | paramManifest := allParameters.Manifest() 90 | 91 | if paramManifest == nil { 92 | paramManifest = make([]tools.ParameterManifest, 0) 93 | } 94 | 95 | // Create MCP manifest 96 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 97 | 98 | // finish tool setup 99 | return Tool{ 100 | Name: cfg.Name, 101 | Kind: kind, 102 | AuthRequired: cfg.AuthRequired, 103 | Collection: cfg.Collection, 104 | FilterPayload: cfg.FilterPayload, 105 | FilterParams: cfg.FilterParams, 106 | AllParams: allParameters, 107 | database: s.Client.Database(cfg.Database), 108 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 109 | mcpManifest: mcpManifest, 110 | }, nil 111 | } 112 | 113 | // validate interface 114 | var _ tools.Tool = Tool{} 115 | 116 | type Tool struct { 117 | Name string `yaml:"name"` 118 | Kind string `yaml:"kind"` 119 | AuthRequired []string `yaml:"authRequired"` 120 | Description string `yaml:"description"` 121 | Collection string `yaml:"collection"` 122 | FilterPayload string `yaml:"filterPayload"` 123 | FilterParams tools.Parameters `yaml:"filterParams"` 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 | filterString, err := tools.PopulateTemplateWithJSON("MongoDBDeleteOneFilter", t.FilterPayload, paramsMap) 135 | if err != nil { 136 | return nil, fmt.Errorf("error populating filter: %s", err) 137 | } 138 | 139 | opts := options.Delete() 140 | 141 | var filter = bson.D{} 142 | err = bson.UnmarshalExtJSON([]byte(filterString), false, &filter) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | res, err := t.database.Collection(t.Collection).DeleteOne(ctx, filter, opts) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | // do not return an error when the count is 0, to mirror the delete many call result 153 | return res.DeletedCount, nil 154 | } 155 | 156 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 157 | return tools.ParseParams(t.AllParams, data, claims) 158 | } 159 | 160 | func (t Tool) Manifest() tools.Manifest { 161 | return t.manifest 162 | } 163 | 164 | func (t Tool) McpManifest() tools.McpManifest { 165 | return t.mcpManifest 166 | } 167 | 168 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 169 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 170 | } 171 | 172 | func (t Tool) RequiresClientAuthorization() bool { 173 | return false 174 | } 175 | ``` -------------------------------------------------------------------------------- /internal/tools/clickhouse/clickhouseexecutesql/clickhouseexecutesql.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 executeSQLKind string = "clickhouse-execute-sql" 34 | 35 | func init() { 36 | if !tools.Register(executeSQLKind, newExecuteSQLConfig) { 37 | panic(fmt.Sprintf("tool kind %q already registered", executeSQLKind)) 38 | } 39 | } 40 | 41 | func newExecuteSQLConfig(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 | AuthRequired []string `yaml:"authRequired"` 55 | } 56 | 57 | var _ tools.ToolConfig = Config{} 58 | 59 | func (cfg Config) ToolConfigKind() string { 60 | return executeSQLKind 61 | } 62 | 63 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 64 | rawS, ok := srcs[cfg.Source] 65 | if !ok { 66 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 67 | } 68 | 69 | s, ok := rawS.(compatibleSource) 70 | if !ok { 71 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", executeSQLKind, compatibleSources) 72 | } 73 | 74 | sqlParameter := tools.NewStringParameter("sql", "The SQL statement to execute.") 75 | parameters := tools.Parameters{sqlParameter} 76 | 77 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 78 | 79 | t := ExecuteSQLTool{ 80 | Name: cfg.Name, 81 | Kind: executeSQLKind, 82 | Parameters: parameters, 83 | AuthRequired: cfg.AuthRequired, 84 | Pool: s.ClickHousePool(), 85 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 86 | mcpManifest: mcpManifest, 87 | } 88 | return t, nil 89 | } 90 | 91 | var _ tools.Tool = ExecuteSQLTool{} 92 | 93 | type ExecuteSQLTool struct { 94 | Name string `yaml:"name"` 95 | Kind string `yaml:"kind"` 96 | AuthRequired []string `yaml:"authRequired"` 97 | Parameters tools.Parameters `yaml:"parameters"` 98 | 99 | Pool *sql.DB 100 | manifest tools.Manifest 101 | mcpManifest tools.McpManifest 102 | } 103 | 104 | func (t ExecuteSQLTool) Invoke(ctx context.Context, params tools.ParamValues, token tools.AccessToken) (any, error) { 105 | paramsMap := params.AsMap() 106 | sql, ok := paramsMap["sql"].(string) 107 | if !ok { 108 | return nil, fmt.Errorf("unable to cast sql parameter %s", paramsMap["sql"]) 109 | } 110 | 111 | results, err := t.Pool.QueryContext(ctx, sql) 112 | if err != nil { 113 | return nil, fmt.Errorf("unable to execute query: %w", err) 114 | } 115 | defer results.Close() 116 | 117 | cols, err := results.Columns() 118 | if err != nil { 119 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 120 | } 121 | 122 | // create an array of values for each column, which can be re-used to scan each row 123 | rawValues := make([]any, len(cols)) 124 | values := make([]any, len(cols)) 125 | for i := range rawValues { 126 | values[i] = &rawValues[i] 127 | } 128 | 129 | colTypes, err := results.ColumnTypes() 130 | if err != nil { 131 | return nil, fmt.Errorf("unable to get column types: %w", err) 132 | } 133 | 134 | var out []any 135 | for results.Next() { 136 | err := results.Scan(values...) 137 | if err != nil { 138 | return nil, fmt.Errorf("unable to parse row: %w", err) 139 | } 140 | vMap := make(map[string]any) 141 | for i, name := range cols { 142 | // ClickHouse driver may return specific types that need handling 143 | switch colTypes[i].DatabaseTypeName() { 144 | case "String", "FixedString": 145 | if rawValues[i] != nil { 146 | // Handle potential []byte to string conversion if needed 147 | if b, ok := rawValues[i].([]byte); ok { 148 | vMap[name] = string(b) 149 | } else { 150 | vMap[name] = rawValues[i] 151 | } 152 | } else { 153 | vMap[name] = nil 154 | } 155 | default: 156 | vMap[name] = rawValues[i] 157 | } 158 | } 159 | out = append(out, vMap) 160 | } 161 | 162 | if err := results.Err(); err != nil { 163 | return nil, fmt.Errorf("errors encountered by results.Scan: %w", err) 164 | } 165 | 166 | return out, nil 167 | } 168 | 169 | func (t ExecuteSQLTool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 170 | return tools.ParseParams(t.Parameters, data, claims) 171 | } 172 | 173 | func (t ExecuteSQLTool) Manifest() tools.Manifest { 174 | return t.manifest 175 | } 176 | 177 | func (t ExecuteSQLTool) McpManifest() tools.McpManifest { 178 | return t.mcpManifest 179 | } 180 | 181 | func (t ExecuteSQLTool) Authorized(verifiedAuthServices []string) bool { 182 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 183 | } 184 | 185 | func (t ExecuteSQLTool) RequiresClientAuthorization() bool { 186 | return false 187 | } 188 | ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.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 mongodbdeletemany 15 | 16 | import ( 17 | "context" 18 | "errors" 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 | "go.mongodb.org/mongo-driver/mongo/options" 27 | 28 | "github.com/googleapis/genai-toolbox/internal/sources" 29 | "github.com/googleapis/genai-toolbox/internal/tools" 30 | ) 31 | 32 | const kind string = "mongodb-delete-many" 33 | 34 | func init() { 35 | if !tools.Register(kind, newConfig) { 36 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 37 | } 38 | } 39 | 40 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 41 | actual := Config{Name: name} 42 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 43 | return nil, err 44 | } 45 | return actual, nil 46 | } 47 | 48 | type Config struct { 49 | Name string `yaml:"name" validate:"required"` 50 | Kind string `yaml:"kind" validate:"required"` 51 | Source string `yaml:"source" validate:"required"` 52 | AuthRequired []string `yaml:"authRequired" validate:"required"` 53 | Description string `yaml:"description" validate:"required"` 54 | Database string `yaml:"database" validate:"required"` 55 | Collection string `yaml:"collection" validate:"required"` 56 | FilterPayload string `yaml:"filterPayload" validate:"required"` 57 | FilterParams tools.Parameters `yaml:"filterParams" validate:"required"` 58 | } 59 | 60 | // validate interface 61 | var _ tools.ToolConfig = Config{} 62 | 63 | func (cfg Config) ToolConfigKind() string { 64 | return kind 65 | } 66 | 67 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 68 | // verify source exists 69 | rawS, ok := srcs[cfg.Source] 70 | if !ok { 71 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 72 | } 73 | 74 | // verify the source is compatible 75 | s, ok := rawS.(*mongosrc.Source) 76 | if !ok { 77 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind) 78 | } 79 | 80 | // Create a slice for all parameters 81 | allParameters := slices.Concat(cfg.FilterParams) 82 | 83 | // Verify no duplicate parameter names 84 | err := tools.CheckDuplicateParameters(allParameters) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // Create Toolbox manifest 90 | paramManifest := allParameters.Manifest() 91 | 92 | if paramManifest == nil { 93 | paramManifest = make([]tools.ParameterManifest, 0) 94 | } 95 | 96 | // Create MCP manifest 97 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 98 | 99 | // finish tool setup 100 | return Tool{ 101 | Name: cfg.Name, 102 | Kind: kind, 103 | AuthRequired: cfg.AuthRequired, 104 | Collection: cfg.Collection, 105 | FilterPayload: cfg.FilterPayload, 106 | FilterParams: cfg.FilterParams, 107 | AllParams: allParameters, 108 | database: s.Client.Database(cfg.Database), 109 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 110 | mcpManifest: mcpManifest, 111 | }, nil 112 | } 113 | 114 | // validate interface 115 | var _ tools.Tool = Tool{} 116 | 117 | type Tool struct { 118 | Name string `yaml:"name"` 119 | Kind string `yaml:"kind"` 120 | AuthRequired []string `yaml:"authRequired"` 121 | Description string `yaml:"description"` 122 | Collection string `yaml:"collection"` 123 | FilterPayload string `yaml:"filterPayload"` 124 | FilterParams tools.Parameters `yaml:"filterParams"` 125 | AllParams tools.Parameters `yaml:"allParams"` 126 | 127 | database *mongo.Database 128 | manifest tools.Manifest 129 | mcpManifest tools.McpManifest 130 | } 131 | 132 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 133 | paramsMap := params.AsMap() 134 | 135 | filterString, err := tools.PopulateTemplateWithJSON("MongoDBDeleteManyFilter", t.FilterPayload, paramsMap) 136 | if err != nil { 137 | return nil, fmt.Errorf("error populating filter: %s", err) 138 | } 139 | 140 | opts := options.Delete() 141 | 142 | var filter = bson.D{} 143 | err = bson.UnmarshalExtJSON([]byte(filterString), false, &filter) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | res, err := t.database.Collection(t.Collection).DeleteMany(ctx, filter, opts) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | if res.DeletedCount == 0 { 154 | return nil, errors.New("no document found") 155 | } 156 | 157 | // not much to return actually 158 | return res.DeletedCount, nil 159 | } 160 | 161 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 162 | return tools.ParseParams(t.AllParams, data, claims) 163 | } 164 | 165 | func (t Tool) Manifest() tools.Manifest { 166 | return t.manifest 167 | } 168 | 169 | func (t Tool) McpManifest() tools.McpManifest { 170 | return t.mcpManifest 171 | } 172 | 173 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 174 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 175 | } 176 | 177 | func (t Tool) RequiresClientAuthorization() bool { 178 | return false 179 | } 180 | ``` -------------------------------------------------------------------------------- /internal/sources/cloudsqlpg/cloud_sql_pg.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 cloudsqlpg 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net" 21 | 22 | "cloud.google.com/go/cloudsqlconn" 23 | "github.com/goccy/go-yaml" 24 | "github.com/googleapis/genai-toolbox/internal/sources" 25 | "github.com/googleapis/genai-toolbox/internal/util" 26 | "github.com/jackc/pgx/v5/pgxpool" 27 | "go.opentelemetry.io/otel/trace" 28 | ) 29 | 30 | const SourceKind string = "cloud-sql-postgres" 31 | 32 | // validate interface 33 | var _ sources.SourceConfig = Config{} 34 | 35 | func init() { 36 | if !sources.Register(SourceKind, newConfig) { 37 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 38 | } 39 | } 40 | 41 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 42 | actual := Config{Name: name, IPType: "public"} // Default IPType 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 | Project string `yaml:"project" validate:"required"` 53 | Region string `yaml:"region" validate:"required"` 54 | Instance string `yaml:"instance" validate:"required"` 55 | IPType sources.IPType `yaml:"ipType" validate:"required"` 56 | Database string `yaml:"database" validate:"required"` 57 | User string `yaml:"user"` 58 | Password string `yaml:"password"` 59 | } 60 | 61 | func (r Config) SourceConfigKind() string { 62 | return SourceKind 63 | } 64 | 65 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 66 | pool, err := initCloudSQLPgConnectionPool(ctx, tracer, r.Name, r.Project, r.Region, r.Instance, r.IPType.String(), r.User, r.Password, r.Database) 67 | if err != nil { 68 | return nil, fmt.Errorf("unable to create pool: %w", err) 69 | } 70 | 71 | err = pool.Ping(ctx) 72 | if err != nil { 73 | return nil, fmt.Errorf("unable to connect successfully: %w", err) 74 | } 75 | 76 | s := &Source{ 77 | Name: r.Name, 78 | Kind: SourceKind, 79 | Pool: pool, 80 | } 81 | return s, nil 82 | } 83 | 84 | var _ sources.Source = &Source{} 85 | 86 | type Source struct { 87 | Name string `yaml:"name"` 88 | Kind string `yaml:"kind"` 89 | Pool *pgxpool.Pool 90 | } 91 | 92 | func (s *Source) SourceKind() string { 93 | return SourceKind 94 | } 95 | 96 | func (s *Source) PostgresPool() *pgxpool.Pool { 97 | return s.Pool 98 | } 99 | 100 | func getConnectionConfig(ctx context.Context, user, pass, dbname string) (string, bool, error) { 101 | userAgent, err := util.UserAgentFromContext(ctx) 102 | if err != nil { 103 | userAgent = "genai-toolbox" 104 | } 105 | useIAM := true 106 | 107 | // If username and password both provided, use password authentication 108 | if user != "" && pass != "" { 109 | dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable application_name=%s", user, pass, dbname, userAgent) 110 | useIAM = false 111 | return dsn, useIAM, nil 112 | } 113 | 114 | // If username is empty, fetch email from ADC 115 | // otherwise, use username as IAM email 116 | if user == "" { 117 | if pass != "" { 118 | // If password is provided without an username, raise an error 119 | return "", useIAM, fmt.Errorf("password is provided without a username. Please provide both a username and password, or leave both fields empty") 120 | } 121 | email, err := sources.GetIAMPrincipalEmailFromADC(ctx) 122 | if err != nil { 123 | return "", useIAM, fmt.Errorf("error getting email from ADC: %v", err) 124 | } 125 | user = email 126 | } 127 | 128 | // Construct IAM connection string with username 129 | dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable application_name=%s", user, dbname, userAgent) 130 | return dsn, useIAM, nil 131 | } 132 | 133 | func initCloudSQLPgConnectionPool(ctx context.Context, tracer trace.Tracer, name, project, region, instance, ipType, user, pass, dbname string) (*pgxpool.Pool, error) { 134 | //nolint:all // Reassigned ctx 135 | ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name) 136 | defer span.End() 137 | 138 | // Configure the driver to connect to the database 139 | dsn, useIAM, err := getConnectionConfig(ctx, user, pass, dbname) 140 | if err != nil { 141 | return nil, fmt.Errorf("unable to get Cloud SQL connection config: %w", err) 142 | } 143 | 144 | config, err := pgxpool.ParseConfig(dsn) 145 | if err != nil { 146 | return nil, fmt.Errorf("unable to parse connection uri: %w", err) 147 | } 148 | 149 | // Create a new dialer with options 150 | userAgent, err := util.UserAgentFromContext(ctx) 151 | if err != nil { 152 | return nil, err 153 | } 154 | opts, err := sources.GetCloudSQLOpts(ipType, userAgent, useIAM) 155 | if err != nil { 156 | return nil, err 157 | } 158 | d, err := cloudsqlconn.NewDialer(ctx, opts...) 159 | if err != nil { 160 | return nil, fmt.Errorf("unable to parse connection uri: %w", err) 161 | } 162 | 163 | // Tell the driver to use the Cloud SQL Go Connector to create connections 164 | i := fmt.Sprintf("%s:%s:%s", project, region, instance) 165 | config.ConnConfig.DialFunc = func(ctx context.Context, _ string, instance string) (net.Conn, error) { 166 | return d.Dial(ctx, i) 167 | } 168 | 169 | // Interact with the driver directly as you normally would 170 | pool, err := pgxpool.NewWithConfig(ctx, config) 171 | if err != nil { 172 | return nil, err 173 | } 174 | return pool, nil 175 | } 176 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookergetexplores/lookergetexplores.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 lookergetexplores 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-explores" 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 | modelParameter := tools.NewStringParameter("model", "The model containing the explores.") 76 | parameters := tools.Parameters{modelParameter} 77 | 78 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 79 | 80 | // finish tool setup 81 | return Tool{ 82 | Name: cfg.Name, 83 | Kind: kind, 84 | Parameters: parameters, 85 | AuthRequired: cfg.AuthRequired, 86 | UseClientOAuth: s.UseClientOAuth, 87 | Client: s.Client, 88 | ApiSettings: s.ApiSettings, 89 | manifest: tools.Manifest{ 90 | Description: cfg.Description, 91 | Parameters: parameters.Manifest(), 92 | AuthRequired: cfg.AuthRequired, 93 | }, 94 | mcpManifest: mcpManifest, 95 | ShowHiddenExplores: s.ShowHiddenExplores, 96 | }, nil 97 | } 98 | 99 | // validate interface 100 | var _ tools.Tool = Tool{} 101 | 102 | type Tool struct { 103 | Name string `yaml:"name"` 104 | Kind string `yaml:"kind"` 105 | UseClientOAuth bool 106 | Client *v4.LookerSDK 107 | ApiSettings *rtl.ApiSettings 108 | AuthRequired []string `yaml:"authRequired"` 109 | Parameters tools.Parameters `yaml:"parameters"` 110 | manifest tools.Manifest 111 | mcpManifest tools.McpManifest 112 | ShowHiddenExplores bool 113 | } 114 | 115 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 116 | logger, err := util.LoggerFromContext(ctx) 117 | if err != nil { 118 | return nil, fmt.Errorf("unable to get logger from ctx: %s", err) 119 | } 120 | mapParams := params.AsMap() 121 | model, ok := mapParams["model"].(string) 122 | if !ok { 123 | return nil, fmt.Errorf("'model' must be a string, got %T", mapParams["model"]) 124 | } 125 | 126 | sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) 127 | if err != nil { 128 | return nil, fmt.Errorf("error getting sdk: %w", err) 129 | } 130 | resp, err := sdk.LookmlModel(model, "explores(name,description,label,group_label,hidden)", t.ApiSettings) 131 | if err != nil { 132 | return nil, fmt.Errorf("error making get_explores request: %s", err) 133 | } 134 | 135 | var data []any 136 | for _, v := range *resp.Explores { 137 | logger.DebugContext(ctx, "Got response element of %v\n", v) 138 | if !t.ShowHiddenExplores && v.Hidden != nil && *v.Hidden { 139 | continue 140 | } 141 | vMap := make(map[string]any) 142 | if v.Name != nil { 143 | vMap["name"] = *v.Name 144 | } 145 | if v.Description != nil { 146 | vMap["description"] = *v.Description 147 | } 148 | if v.Label != nil { 149 | vMap["label"] = *v.Label 150 | } 151 | if v.GroupLabel != nil { 152 | vMap["group_label"] = *v.GroupLabel 153 | } 154 | logger.DebugContext(ctx, "Converted to %v\n", vMap) 155 | data = append(data, vMap) 156 | } 157 | logger.DebugContext(ctx, "data = ", data) 158 | 159 | return data, nil 160 | } 161 | 162 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 163 | return tools.ParseParams(t.Parameters, data, claims) 164 | } 165 | 166 | func (t Tool) Manifest() tools.Manifest { 167 | return t.manifest 168 | } 169 | 170 | func (t Tool) McpManifest() tools.McpManifest { 171 | return t.mcpManifest 172 | } 173 | 174 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 175 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 176 | } 177 | 178 | func (t Tool) RequiresClientAuthorization() bool { 179 | return t.UseClientOAuth 180 | } 181 | ``` -------------------------------------------------------------------------------- /tests/cloudsql/cloudsql_list_instances_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 cloudsql 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httptest" 25 | "net/url" 26 | "reflect" 27 | "regexp" 28 | "strings" 29 | "testing" 30 | "time" 31 | 32 | "github.com/googleapis/genai-toolbox/internal/testutils" 33 | _ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqllistinstances" 34 | "github.com/googleapis/genai-toolbox/tests" 35 | ) 36 | 37 | type transport struct { 38 | transport http.RoundTripper 39 | url *url.URL 40 | } 41 | 42 | func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { 43 | if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { 44 | req.URL.Scheme = t.url.Scheme 45 | req.URL.Host = t.url.Host 46 | } 47 | return t.transport.RoundTrip(req) 48 | } 49 | 50 | func TestListInstance(t *testing.T) { 51 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | if !strings.Contains(r.UserAgent(), "genai-toolbox/") { 53 | t.Errorf("User-Agent header not found") 54 | } 55 | if r.URL.Path != "/v1/projects/test-project/instances" { 56 | http.Error(w, fmt.Sprintf("unexpected path: got %q", r.URL.Path), http.StatusBadRequest) 57 | return 58 | } 59 | w.Header().Set("Content-Type", "application/json") 60 | fmt.Fprintln(w, `{"items": [{"name": "test-instance", "instanceType": "CLOUD_SQL_INSTANCE"}]}`) 61 | })) 62 | defer server.Close() 63 | 64 | serverURL, err := url.Parse(server.URL) 65 | if err != nil { 66 | t.Fatalf("failed to parse server URL: %v", err) 67 | } 68 | 69 | originalTransport := http.DefaultClient.Transport 70 | if originalTransport == nil { 71 | originalTransport = http.DefaultTransport 72 | } 73 | http.DefaultClient.Transport = &transport{ 74 | transport: originalTransport, 75 | url: serverURL, 76 | } 77 | t.Cleanup(func() { 78 | http.DefaultClient.Transport = originalTransport 79 | }) 80 | 81 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 82 | defer cancel() 83 | 84 | var args []string 85 | 86 | toolsFile := getListInstanceToolsConfig() 87 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 88 | if err != nil { 89 | t.Fatalf("command initialization returned an error: %s", err) 90 | } 91 | defer cleanup() 92 | 93 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 94 | defer cancel() 95 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile("Server ready to serve"), cmd.Out) 96 | if err != nil { 97 | t.Logf("toolbox command logs: \n%s", out) 98 | t.Fatalf("toolbox didn't start successfully: %s", err) 99 | } 100 | 101 | tcs := []struct { 102 | name string 103 | toolName string 104 | body string 105 | want string 106 | expectError bool 107 | }{ 108 | { 109 | name: "successful operation", 110 | toolName: "list-instances", 111 | body: `{"project": "test-project"}`, 112 | want: `[{"name":"test-instance","instanceType":"CLOUD_SQL_INSTANCE"}]`, 113 | }, 114 | { 115 | name: "failed operation", 116 | toolName: "list-instances-fail", 117 | body: `{"project": "test-project"}`, 118 | expectError: true, 119 | }, 120 | } 121 | 122 | for _, tc := range tcs { 123 | t.Run(tc.name, func(t *testing.T) { 124 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 125 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 126 | if err != nil { 127 | t.Fatalf("unable to create request: %s", err) 128 | } 129 | req.Header.Add("Content-type", "application/json") 130 | resp, err := http.DefaultClient.Do(req) 131 | if err != nil { 132 | t.Fatalf("unable to send request: %s", err) 133 | } 134 | defer resp.Body.Close() 135 | 136 | if tc.expectError { 137 | if resp.StatusCode == http.StatusOK { 138 | t.Fatal("expected error but got status 200") 139 | } 140 | return 141 | } 142 | 143 | if resp.StatusCode != http.StatusOK { 144 | bodyBytes, _ := io.ReadAll(resp.Body) 145 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 146 | } 147 | 148 | var result struct { 149 | Result string `json:"result"` 150 | } 151 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 152 | t.Fatalf("failed to decode response: %v", err) 153 | } 154 | 155 | var got, want any 156 | if err := json.Unmarshal([]byte(result.Result), &got); err != nil { 157 | t.Fatalf("failed to unmarshal result: %v", err) 158 | } 159 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 160 | t.Fatalf("failed to unmarshal want: %v", err) 161 | } 162 | 163 | if !reflect.DeepEqual(got, want) { 164 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 165 | } 166 | }) 167 | } 168 | } 169 | 170 | func getListInstanceToolsConfig() map[string]any { 171 | return map[string]any{ 172 | "sources": map[string]any{ 173 | "my-cloud-sql-source": map[string]any{ 174 | "kind": "cloud-sql-admin", 175 | }, 176 | "my-invalid-cloud-sql-source": map[string]any{ 177 | "kind": "cloud-sql-admin", 178 | "useClientOAuth": true, 179 | }, 180 | }, 181 | "tools": map[string]any{ 182 | "list-instances": map[string]any{ 183 | "kind": "cloud-sql-list-instances", 184 | "source": "my-cloud-sql-source", 185 | }, 186 | "list-instances-fail": map[string]any{ 187 | "kind": "cloud-sql-list-instances", 188 | "description": "list instances", 189 | "source": "my-invalid-cloud-sql-source", 190 | }, 191 | }, 192 | } 193 | } 194 | ``` -------------------------------------------------------------------------------- /docs/en/getting-started/quickstart/go/genAI/quickstart.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "github.com/googleapis/mcp-toolbox-sdk-go/core" 11 | "google.golang.org/genai" 12 | ) 13 | 14 | // ConvertToGenaiTool translates a ToolboxTool into the genai.FunctionDeclaration format. 15 | func ConvertToGenaiTool(toolboxTool *core.ToolboxTool) *genai.Tool { 16 | 17 | inputschema, err := toolboxTool.InputSchema() 18 | if err != nil { 19 | return &genai.Tool{} 20 | } 21 | 22 | var paramsSchema *genai.Schema 23 | _ = json.Unmarshal(inputschema, ¶msSchema) 24 | // First, create the function declaration. 25 | funcDeclaration := &genai.FunctionDeclaration{ 26 | Name: toolboxTool.Name(), 27 | Description: toolboxTool.Description(), 28 | Parameters: paramsSchema, 29 | } 30 | 31 | // Then, wrap the function declaration in a genai.Tool struct. 32 | return &genai.Tool{ 33 | FunctionDeclarations: []*genai.FunctionDeclaration{funcDeclaration}, 34 | } 35 | } 36 | 37 | func printResponse(resp *genai.GenerateContentResponse) { 38 | for _, cand := range resp.Candidates { 39 | if cand.Content != nil { 40 | for _, part := range cand.Content.Parts { 41 | fmt.Println(part.Text) 42 | } 43 | } 44 | } 45 | } 46 | 47 | const systemPrompt = ` 48 | You're a helpful hotel assistant. You handle hotel searching, booking, and 49 | cancellations. When the user searches for a hotel, mention its name, id, 50 | location and price tier. Always mention hotel ids while performing any 51 | searches. This is very important for any operations. For any bookings or 52 | cancellations, please provide the appropriate confirmation. Be sure to 53 | update checkin or checkout dates if mentioned by the user. 54 | Don't ask for confirmations from the user. 55 | ` 56 | 57 | var queries = []string{ 58 | "Find hotels in Basel with Basel in its name.", 59 | "Can you book the hotel Hilton Basel for me?", 60 | "Oh wait, this is too expensive. Please cancel it.", 61 | "Please book the Hyatt Regency instead.", 62 | "My check in dates would be from April 10, 2024 to April 19, 2024.", 63 | } 64 | 65 | func main() { 66 | // Setup 67 | ctx := context.Background() 68 | apiKey := os.Getenv("GOOGLE_API_KEY") 69 | toolboxURL := "http://localhost:5000" 70 | 71 | // Initialize the Google GenAI client using the explicit ClientConfig. 72 | client, err := genai.NewClient(ctx, &genai.ClientConfig{ 73 | APIKey: apiKey, 74 | }) 75 | if err != nil { 76 | log.Fatalf("Failed to create Google GenAI client: %v", err) 77 | } 78 | 79 | // Initialize the MCP Toolbox client. 80 | toolboxClient, err := core.NewToolboxClient(toolboxURL) 81 | if err != nil { 82 | log.Fatalf("Failed to create Toolbox client: %v", err) 83 | } 84 | 85 | // Load the tool using the MCP Toolbox SDK. 86 | tools, err := toolboxClient.LoadToolset("my-toolset", ctx) 87 | if err != nil { 88 | log.Fatalf("Failed to load tools: %v\nMake sure your Toolbox server is running and the tool is configured.", err) 89 | } 90 | 91 | genAITools := make([]*genai.Tool, len(tools)) 92 | toolsMap := make(map[string]*core.ToolboxTool, len(tools)) 93 | 94 | for i, tool := range tools { 95 | genAITools[i] = ConvertToGenaiTool(tool) 96 | toolsMap[tool.Name()] = tool 97 | } 98 | 99 | // Set up the generative model with the available tool. 100 | modelName := "gemini-2.0-flash" 101 | 102 | // Create the initial content prompt for the model. 103 | messageHistory := []*genai.Content{ 104 | genai.NewContentFromText(systemPrompt, genai.RoleUser), 105 | } 106 | config := &genai.GenerateContentConfig{ 107 | Tools: genAITools, 108 | ToolConfig: &genai.ToolConfig{ 109 | FunctionCallingConfig: &genai.FunctionCallingConfig{ 110 | Mode: genai.FunctionCallingConfigModeAny, 111 | }, 112 | }, 113 | } 114 | 115 | for _, query := range queries { 116 | 117 | messageHistory = append(messageHistory, genai.NewContentFromText(query, genai.RoleUser)) 118 | 119 | genContentResp, err := client.Models.GenerateContent(ctx, modelName, messageHistory, config) 120 | if err != nil { 121 | log.Fatalf("LLM call failed for query '%s': %v", query, err) 122 | } 123 | 124 | if len(genContentResp.Candidates) > 0 && genContentResp.Candidates[0].Content != nil { 125 | messageHistory = append(messageHistory, genContentResp.Candidates[0].Content) 126 | } 127 | 128 | functionCalls := genContentResp.FunctionCalls() 129 | 130 | toolResponseParts := []*genai.Part{} 131 | 132 | for _, fc := range functionCalls { 133 | 134 | toolToInvoke, found := toolsMap[fc.Name] 135 | if !found { 136 | log.Fatalf("Tool '%s' not found in loaded tools map. Check toolset configuration.", fc.Name) 137 | } 138 | 139 | toolResult, invokeErr := toolToInvoke.Invoke(ctx, fc.Args) 140 | if invokeErr != nil { 141 | log.Fatalf("Failed to execute tool '%s': %v", fc.Name, invokeErr) 142 | } 143 | 144 | // Enhanced Tool Result Handling (retained to prevent nil issues) 145 | toolResultString := "" 146 | if toolResult != nil { 147 | jsonBytes, marshalErr := json.Marshal(toolResult) 148 | if marshalErr == nil { 149 | toolResultString = string(jsonBytes) 150 | } else { 151 | toolResultString = fmt.Sprintf("%v", toolResult) 152 | } 153 | } 154 | 155 | responseMap := map[string]any{"result": toolResultString} 156 | 157 | toolResponseParts = append(toolResponseParts, genai.NewPartFromFunctionResponse(fc.Name, responseMap)) 158 | } 159 | // Add all accumulated tool responses for this turn to the message history. 160 | toolResponseContent := genai.NewContentFromParts(toolResponseParts, "function") 161 | messageHistory = append(messageHistory, toolResponseContent) 162 | 163 | finalResponse, err := client.Models.GenerateContent(ctx, modelName, messageHistory, &genai.GenerateContentConfig{}) 164 | if err != nil { 165 | log.Fatalf("Error calling GenerateContent (with function result): %v", err) 166 | } 167 | 168 | printResponse(finalResponse) 169 | // Add the final textual response from the LLM to the history 170 | if len(finalResponse.Candidates) > 0 && finalResponse.Candidates[0].Content != nil { 171 | messageHistory = append(messageHistory, finalResponse.Candidates[0].Content) 172 | } 173 | } 174 | } 175 | ``` -------------------------------------------------------------------------------- /internal/sources/alloydbpg/alloydb_pg_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 alloydbpg_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" 24 | "github.com/googleapis/genai-toolbox/internal/sources/alloydbpg" 25 | "github.com/googleapis/genai-toolbox/internal/testutils" 26 | ) 27 | 28 | func TestParseFromYamlAlloyDBPg(t *testing.T) { 29 | tcs := []struct { 30 | desc string 31 | in string 32 | want server.SourceConfigs 33 | }{ 34 | { 35 | desc: "basic example", 36 | in: ` 37 | sources: 38 | my-pg-instance: 39 | kind: alloydb-postgres 40 | project: my-project 41 | region: my-region 42 | cluster: my-cluster 43 | instance: my-instance 44 | database: my_db 45 | user: my_user 46 | password: my_pass 47 | `, 48 | want: map[string]sources.SourceConfig{ 49 | "my-pg-instance": alloydbpg.Config{ 50 | Name: "my-pg-instance", 51 | Kind: alloydbpg.SourceKind, 52 | Project: "my-project", 53 | Region: "my-region", 54 | Cluster: "my-cluster", 55 | Instance: "my-instance", 56 | IPType: "public", 57 | Database: "my_db", 58 | User: "my_user", 59 | Password: "my_pass", 60 | }, 61 | }, 62 | }, 63 | { 64 | desc: "public ipType", 65 | in: ` 66 | sources: 67 | my-pg-instance: 68 | kind: alloydb-postgres 69 | project: my-project 70 | region: my-region 71 | cluster: my-cluster 72 | instance: my-instance 73 | ipType: Public 74 | database: my_db 75 | user: my_user 76 | password: my_pass 77 | `, 78 | want: map[string]sources.SourceConfig{ 79 | "my-pg-instance": alloydbpg.Config{ 80 | Name: "my-pg-instance", 81 | Kind: alloydbpg.SourceKind, 82 | Project: "my-project", 83 | Region: "my-region", 84 | Cluster: "my-cluster", 85 | Instance: "my-instance", 86 | IPType: "public", 87 | Database: "my_db", 88 | User: "my_user", 89 | Password: "my_pass", 90 | }, 91 | }, 92 | }, 93 | { 94 | desc: "private ipType", 95 | in: ` 96 | sources: 97 | my-pg-instance: 98 | kind: alloydb-postgres 99 | project: my-project 100 | region: my-region 101 | cluster: my-cluster 102 | instance: my-instance 103 | ipType: private 104 | database: my_db 105 | user: my_user 106 | password: my_pass 107 | `, 108 | want: map[string]sources.SourceConfig{ 109 | "my-pg-instance": alloydbpg.Config{ 110 | Name: "my-pg-instance", 111 | Kind: alloydbpg.SourceKind, 112 | Project: "my-project", 113 | Region: "my-region", 114 | Cluster: "my-cluster", 115 | Instance: "my-instance", 116 | IPType: "private", 117 | Database: "my_db", 118 | User: "my_user", 119 | Password: "my_pass", 120 | }, 121 | }, 122 | }, 123 | } 124 | for _, tc := range tcs { 125 | t.Run(tc.desc, func(t *testing.T) { 126 | got := struct { 127 | Sources server.SourceConfigs `yaml:"sources"` 128 | }{} 129 | // Parse contents 130 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 131 | if err != nil { 132 | t.Fatalf("unable to unmarshal: %s", err) 133 | } 134 | if !cmp.Equal(tc.want, got.Sources) { 135 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestFailParseFromYaml(t *testing.T) { 142 | tcs := []struct { 143 | desc string 144 | in string 145 | err string 146 | }{ 147 | { 148 | desc: "invalid ipType", 149 | in: ` 150 | sources: 151 | my-pg-instance: 152 | kind: alloydb-postgres 153 | project: my-project 154 | region: my-region 155 | cluster: my-cluster 156 | instance: my-instance 157 | ipType: fail 158 | database: my_db 159 | user: my_user 160 | password: my_pass 161 | `, 162 | err: "unable to parse source \"my-pg-instance\" as \"alloydb-postgres\": ipType invalid: must be one of \"public\", \"private\", or \"psc\"", 163 | }, 164 | { 165 | desc: "extra field", 166 | in: ` 167 | sources: 168 | my-pg-instance: 169 | kind: alloydb-postgres 170 | project: my-project 171 | region: my-region 172 | cluster: my-cluster 173 | instance: my-instance 174 | database: my_db 175 | user: my_user 176 | password: my_pass 177 | foo: bar 178 | `, 179 | err: "unable to parse source \"my-pg-instance\" as \"alloydb-postgres\": [3:1] unknown field \"foo\"\n 1 | cluster: my-cluster\n 2 | database: my_db\n> 3 | foo: bar\n ^\n 4 | instance: my-instance\n 5 | kind: alloydb-postgres\n 6 | password: my_pass\n 7 | ", 180 | }, 181 | { 182 | desc: "missing required field", 183 | in: ` 184 | sources: 185 | my-pg-instance: 186 | kind: alloydb-postgres 187 | region: my-region 188 | cluster: my-cluster 189 | instance: my-instance 190 | database: my_db 191 | user: my_user 192 | password: my_pass 193 | `, 194 | err: "unable to parse source \"my-pg-instance\" as \"alloydb-postgres\": Key: 'Config.Project' Error:Field validation for 'Project' failed on the 'required' tag", 195 | }, 196 | } 197 | for _, tc := range tcs { 198 | t.Run(tc.desc, func(t *testing.T) { 199 | got := struct { 200 | Sources server.SourceConfigs `yaml:"sources"` 201 | }{} 202 | // Parse contents 203 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 204 | if err == nil { 205 | t.Fatalf("expect parsing to fail") 206 | } 207 | errStr := err.Error() 208 | if errStr != tc.err { 209 | t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) 210 | } 211 | }) 212 | } 213 | } 214 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/bigtable/bigtable-sql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "bigtable-sql" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "bigtable-sql" tool executes a pre-defined SQL statement against a Google 7 | Cloud Bigtable instance. 8 | aliases: 9 | - /resources/tools/bigtable-sql 10 | --- 11 | 12 | ## About 13 | 14 | A `bigtable-sql` tool executes a pre-defined SQL statement against a Bigtable 15 | instance. It's compatible with any of the following sources: 16 | 17 | - [bigtable](../../sources/bigtable.md) 18 | 19 | ### GoogleSQL 20 | 21 | Bigtable supports SQL queries. The integration with Toolbox supports `googlesql` 22 | dialect, the specified SQL statement is executed as a [data manipulation 23 | language (DML)][bigtable-googlesql] statements, and specified parameters will 24 | inserted according to their name: e.g. `@name`. 25 | 26 | {{<notice note>}} 27 | Bigtable's GoogleSQL support for DML statements might be limited to certain 28 | query types. For detailed information on supported DML statements and use 29 | cases, refer to the [Bigtable GoogleSQL use 30 | cases](https://cloud.google.com/bigtable/docs/googlesql-overview#use-cases). 31 | {{</notice>}} 32 | 33 | [bigtable-googlesql]: https://cloud.google.com/bigtable/docs/googlesql-overview 34 | 35 | ## Example 36 | 37 | > **Note:** This tool uses parameterized queries to prevent SQL injections. 38 | > Query parameters can be used as substitutes for arbitrary expressions. 39 | > Parameters cannot be used as substitutes for identifiers, column names, table 40 | > names, or other parts of the query. 41 | 42 | ```yaml 43 | tools: 44 | search_user_by_id_or_name: 45 | kind: bigtable-sql 46 | source: my-bigtable-instance 47 | statement: | 48 | SELECT 49 | TO_INT64(cf[ 'id' ]) as id, 50 | CAST(cf[ 'name' ] AS string) as name, 51 | FROM 52 | mytable 53 | WHERE 54 | TO_INT64(cf[ 'id' ]) = @id 55 | OR CAST(cf[ 'name' ] AS string) = @name; 56 | description: | 57 | Use this tool to get information for a specific user. 58 | Takes an id number or a name and returns info on the user. 59 | 60 | Example: 61 | {{ 62 | "id": 123, 63 | "name": "Alice", 64 | }} 65 | parameters: 66 | - name: id 67 | type: integer 68 | description: User ID 69 | - name: name 70 | type: string 71 | description: Name of the user 72 | ``` 73 | 74 | ### Example with Template Parameters 75 | 76 | > **Note:** This tool allows direct modifications to the SQL statement, 77 | > including identifiers, column names, and table names. **This makes it more 78 | > vulnerable to SQL injections**. Using basic parameters only (see above) is 79 | > recommended for performance and safety reasons. For more details, please check 80 | > [templateParameters](..#template-parameters). 81 | 82 | ```yaml 83 | tools: 84 | list_table: 85 | kind: bigtable-sql 86 | source: my-bigtable-instance 87 | statement: | 88 | SELECT * FROM {{.tableName}}; 89 | description: | 90 | Use this tool to list all information from a specific table. 91 | Example: 92 | {{ 93 | "tableName": "flights", 94 | }} 95 | templateParameters: 96 | - name: tableName 97 | type: string 98 | description: Table to select from 99 | ``` 100 | 101 | ## Reference 102 | 103 | | **field** | **type** | **required** | **description** | 104 | |--------------------|:------------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------| 105 | | kind | string | true | Must be "bigtable-sql". | 106 | | source | string | true | Name of the source the SQL should execute on. | 107 | | description | string | true | Description of the tool that is passed to the LLM. | 108 | | statement | string | true | SQL statement to execute on. | 109 | | parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | 110 | | templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | 111 | 112 | ## Tips 113 | 114 | - [Bigtable Studio][bigtable-studio] is a useful to explore and manage your 115 | Bigtable data. If you're unfamiliar with the query syntax, [Query 116 | Builder][bigtable-querybuilder] lets you build a query, run it against a 117 | table, and then view the results in the console. 118 | - Some Python libraries limit the use of underscore columns such as `_key`. A 119 | workaround would be to leverage Bigtable [Logical 120 | Views][bigtable-logical-view] to rename the columns. 121 | 122 | [bigtable-studio]: https://cloud.google.com/bigtable/docs/manage-data-using-console 123 | [bigtable-logical-view]: https://cloud.google.com/bigtable/docs/create-manage-logical-views 124 | [bigtable-querybuilder]: https://cloud.google.com/bigtable/docs/query-builder 125 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/mongodb/mongodb-update-one.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "mongodb-update-one" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "mongodb-update-one" tool updates a single document in a MongoDB collection. 7 | aliases: 8 | - /resources/tools/mongodb-update-one 9 | --- 10 | 11 | ## About 12 | 13 | A `mongodb-update-one` tool updates a single document within a specified MongoDB 14 | collection. It locates the document to be updated using a `filterPayload` and 15 | applies modifications defined in an `updatePayload`. If the filter matches 16 | multiple documents, only the first one found will be updated. 17 | 18 | This tool is compatible with the following source kind: 19 | 20 | * [`mongodb`](../../sources/mongodb.md) 21 | 22 | --- 23 | 24 | ## Example 25 | 26 | Here's an example of a `mongodb-update-one` tool configuration. This tool 27 | updates the `stock` and `status` fields of a document in the `inventory` 28 | collection where the `item` field matches a provided value. If no matching 29 | document is found, the `upsert: true` option will create a new one. 30 | 31 | ```yaml 32 | tools: 33 | update_inventory_item: 34 | kind: mongodb-update-one 35 | source: my-mongo-source 36 | description: Use this tool to update an item's stock and status in the inventory. 37 | database: products 38 | collection: inventory 39 | filterPayload: | 40 | { "item": {{json .item_name}} } 41 | filterParams: 42 | - name: item_name 43 | type: string 44 | description: The name of the item to update. 45 | updatePayload: | 46 | { "$set": { "stock": {{json .new_stock}}, "status": {{json .new_status}} } } 47 | updateParams: 48 | - name: new_stock 49 | type: integer 50 | description: The new stock quantity. 51 | - name: new_status 52 | type: string 53 | description: The new status of the item (e.g., "In Stock", "Backordered"). 54 | canonical: false 55 | upsert: true 56 | ``` 57 | 58 | ## Reference 59 | 60 | | **field** | **type** | **required** | **description** | 61 | |:--------------|:---------|:-------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 62 | | kind | string | true | Must be `mongodb-update-one`. | 63 | | source | string | true | The name of the `mongodb` source to use. | 64 | | description | string | true | A description of the tool that is passed to the LLM. | 65 | | database | string | true | The name of the MongoDB database containing the collection. | 66 | | collection | string | true | The name of the MongoDB collection to update a document in. | 67 | | filterPayload | string | true | The MongoDB query filter document to select the document for updating. It's written as a Go template, using `{{json .param_name}}` to insert parameters. | 68 | | filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. | 69 | | updatePayload | string | true | The MongoDB update document, which specifies the modifications. This often uses update operators like `$set`. It's written as a Go template, using `{{json .param_name}}` to insert parameters. | 70 | | updateParams | list | true | A list of parameter objects that define the variables used in the `updatePayload`. | 71 | | canonical | bool | true | Determines if the `updatePayload` string is parsed using MongoDB's Canonical or Relaxed Extended JSON format. **Canonical** is stricter about type representation (e.g., `{"$numberInt": "42"}`), while **Relaxed** is more lenient (e.g., `42`). | 72 | | upsert | bool | false | If `true`, a new document is created if no document matches the `filterPayload`. Defaults to `false`. | 73 | ``` -------------------------------------------------------------------------------- /internal/tools/neo4j/neo4jexecutecypher/neo4jexecutecypher.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 neo4jexecutecypher 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | neo4jsc "github.com/googleapis/genai-toolbox/internal/sources/neo4j" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jexecutecypher/classifier" 26 | "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jschema/helpers" 27 | "github.com/neo4j/neo4j-go-driver/v5/neo4j" 28 | ) 29 | 30 | const kind string = "neo4j-execute-cypher" 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 | Neo4jDriver() neo4j.DriverWithContext 48 | Neo4jDatabase() string 49 | } 50 | 51 | // validate compatible sources are still compatible 52 | var _ compatibleSource = &neo4jsc.Source{} 53 | 54 | var compatibleSources = [...]string{neo4jsc.SourceKind} 55 | 56 | type Config struct { 57 | Name string `yaml:"name" validate:"required"` 58 | Kind string `yaml:"kind" validate:"required"` 59 | Source string `yaml:"source" validate:"required"` 60 | Description string `yaml:"description" validate:"required"` 61 | ReadOnly bool `yaml:"readOnly"` 62 | AuthRequired []string `yaml:"authRequired"` 63 | } 64 | 65 | // validate interface 66 | var _ tools.ToolConfig = Config{} 67 | 68 | func (cfg Config) ToolConfigKind() string { 69 | return kind 70 | } 71 | 72 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 73 | // verify source exists 74 | rawS, ok := srcs[cfg.Source] 75 | if !ok { 76 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 77 | } 78 | 79 | // verify the source is compatible 80 | var s compatibleSource 81 | s, ok = rawS.(compatibleSource) 82 | if !ok { 83 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 84 | } 85 | 86 | cypherParameter := tools.NewStringParameter("cypher", "The cypher to execute.") 87 | parameters := tools.Parameters{cypherParameter} 88 | 89 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 90 | 91 | // finish tool setup 92 | t := Tool{ 93 | Name: cfg.Name, 94 | Kind: kind, 95 | Parameters: parameters, 96 | AuthRequired: cfg.AuthRequired, 97 | ReadOnly: cfg.ReadOnly, 98 | Driver: s.Neo4jDriver(), 99 | Database: s.Neo4jDatabase(), 100 | classifier: classifier.NewQueryClassifier(), 101 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), 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 | Parameters tools.Parameters `yaml:"parameters"` 114 | AuthRequired []string `yaml:"authRequired"` 115 | ReadOnly bool `yaml:"readOnly"` 116 | Database string 117 | Driver neo4j.DriverWithContext 118 | classifier *classifier.QueryClassifier 119 | manifest tools.Manifest 120 | mcpManifest tools.McpManifest 121 | } 122 | 123 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 124 | paramsMap := params.AsMap() 125 | cypherStr, ok := paramsMap["cypher"].(string) 126 | if !ok { 127 | return nil, fmt.Errorf("unable to get cast %s", paramsMap["cypher"]) 128 | } 129 | 130 | if cypherStr == "" { 131 | return nil, fmt.Errorf("parameter 'cypher' must be a non-empty string") 132 | } 133 | 134 | // validate the cypher query before executing 135 | cf := t.classifier.Classify(cypherStr) 136 | if cf.Error != nil { 137 | return nil, cf.Error 138 | } 139 | 140 | if cf.Type == classifier.WriteQuery && t.ReadOnly { 141 | return nil, fmt.Errorf("this tool is read-only and cannot execute write queries") 142 | } 143 | 144 | config := neo4j.ExecuteQueryWithDatabase(t.Database) 145 | results, err := neo4j.ExecuteQuery(ctx, t.Driver, cypherStr, nil, 146 | neo4j.EagerResultTransformer, config) 147 | if err != nil { 148 | return nil, fmt.Errorf("unable to execute query: %w", err) 149 | } 150 | 151 | var out []any 152 | keys := results.Keys 153 | records := results.Records 154 | for _, record := range records { 155 | vMap := make(map[string]any) 156 | for col, value := range record.Values { 157 | vMap[keys[col]] = helpers.ConvertValue(value) 158 | } 159 | out = append(out, vMap) 160 | } 161 | 162 | return out, nil 163 | } 164 | 165 | func (t Tool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (tools.ParamValues, error) { 166 | return tools.ParseParams(t.Parameters, data, claimsMap) 167 | } 168 | 169 | func (t Tool) Manifest() tools.Manifest { 170 | return t.manifest 171 | } 172 | 173 | func (t Tool) McpManifest() tools.McpManifest { 174 | return t.mcpManifest 175 | } 176 | 177 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 178 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 179 | } 180 | 181 | func (t Tool) RequiresClientAuthorization() bool { 182 | return false 183 | } 184 | ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookercommon/lookercommon_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 lookercommon_test 16 | 17 | import ( 18 | "encoding/json" 19 | "testing" 20 | 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/testutils" 23 | "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" 24 | v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" 25 | ) 26 | 27 | func TestExtractLookerFieldProperties(t *testing.T) { 28 | ctx, err := testutils.ContextWithNewLogger() 29 | if err != nil { 30 | t.Fatalf("unexpected error: %s", err) 31 | } 32 | 33 | // Helper function to create string pointers 34 | stringPtr := func(s string) *string { return &s } 35 | stringArrayPtr := func(s []string) *[]string { return &s } 36 | boolPtr := func(b bool) *bool { return &b } 37 | 38 | tcs := []struct { 39 | desc string 40 | fields []v4.LookmlModelExploreField 41 | want []any 42 | }{ 43 | { 44 | desc: "field with all properties including description", 45 | fields: []v4.LookmlModelExploreField{ 46 | { 47 | Name: stringPtr("dimension_name"), 48 | Type: stringPtr("string"), 49 | Label: stringPtr("Dimension Label"), 50 | LabelShort: stringPtr("Dim Label"), 51 | Description: stringPtr("This is a dimension description"), 52 | Suggestable: boolPtr(true), 53 | SuggestExplore: stringPtr("explore"), 54 | SuggestDimension: stringPtr("dimension"), 55 | Suggestions: stringArrayPtr([]string{"foo", "bar", "baz"}), 56 | }, 57 | }, 58 | want: []any{ 59 | map[string]any{ 60 | "name": "dimension_name", 61 | "type": "string", 62 | "label": "Dimension Label", 63 | "label_short": "Dim Label", 64 | "description": "This is a dimension description", 65 | "suggest_explore": "explore", 66 | "suggest_dimension": "dimension", 67 | "suggestions": []string{"foo", "bar", "baz"}, 68 | }, 69 | }, 70 | }, 71 | { 72 | desc: "field with missing description", 73 | fields: []v4.LookmlModelExploreField{ 74 | { 75 | Name: stringPtr("dimension_name"), 76 | Type: stringPtr("string"), 77 | Label: stringPtr("Dimension Label"), 78 | LabelShort: stringPtr("Dim Label"), 79 | // Description is nil 80 | }, 81 | }, 82 | want: []any{ 83 | map[string]any{ 84 | "name": "dimension_name", 85 | "type": "string", 86 | "label": "Dimension Label", 87 | "label_short": "Dim Label", 88 | // description should not be present in the map 89 | }, 90 | }, 91 | }, 92 | { 93 | desc: "field with only required fields", 94 | fields: []v4.LookmlModelExploreField{ 95 | { 96 | Name: stringPtr("simple_dimension"), 97 | Type: stringPtr("number"), 98 | }, 99 | }, 100 | want: []any{ 101 | map[string]any{ 102 | "name": "simple_dimension", 103 | "type": "number", 104 | }, 105 | }, 106 | }, 107 | { 108 | desc: "empty fields list", 109 | fields: []v4.LookmlModelExploreField{}, 110 | want: []any{}, 111 | }, 112 | { 113 | desc: "multiple fields with mixed properties", 114 | fields: []v4.LookmlModelExploreField{ 115 | { 116 | Name: stringPtr("dim1"), 117 | Type: stringPtr("string"), 118 | Label: stringPtr("First Dimension"), 119 | Description: stringPtr("First dimension description"), 120 | }, 121 | { 122 | Name: stringPtr("dim2"), 123 | Type: stringPtr("number"), 124 | LabelShort: stringPtr("Dim2"), 125 | }, 126 | }, 127 | want: []any{ 128 | map[string]any{ 129 | "name": "dim1", 130 | "type": "string", 131 | "label": "First Dimension", 132 | "description": "First dimension description", 133 | }, 134 | map[string]any{ 135 | "name": "dim2", 136 | "type": "number", 137 | "label_short": "Dim2", 138 | }, 139 | }, 140 | }, 141 | } 142 | 143 | for _, tc := range tcs { 144 | t.Run(tc.desc, func(t *testing.T) { 145 | got, err := lookercommon.ExtractLookerFieldProperties(ctx, &tc.fields, true) 146 | if err != nil { 147 | t.Fatalf("unexpected error: %v", err) 148 | } 149 | 150 | if diff := cmp.Diff(tc.want, got); diff != "" { 151 | t.Fatalf("incorrect result: diff %v", diff) 152 | } 153 | }) 154 | } 155 | } 156 | 157 | func TestExtractLookerFieldPropertiesWithNilFields(t *testing.T) { 158 | ctx, err := testutils.ContextWithNewLogger() 159 | if err != nil { 160 | t.Fatalf("unexpected error: %s", err) 161 | } 162 | 163 | got, err := lookercommon.ExtractLookerFieldProperties(ctx, nil, true) 164 | if err != nil { 165 | t.Fatalf("unexpected error: %v", err) 166 | } 167 | 168 | want := []any{} 169 | if diff := cmp.Diff(want, got); diff != "" { 170 | t.Fatalf("incorrect result: diff %v", diff) 171 | } 172 | } 173 | 174 | func TestRequestRunInlineQuery2(t *testing.T) { 175 | fields := make([]string, 1) 176 | fields[0] = "foo.bar" 177 | wq := v4.WriteQuery{ 178 | Model: "model", 179 | View: "explore", 180 | Fields: &fields, 181 | } 182 | req2 := lookercommon.RequestRunInlineQuery2{ 183 | Query: wq, 184 | RenderOpts: lookercommon.RenderOptions{ 185 | Format: "json", 186 | }, 187 | QueryApiClientCtx: lookercommon.QueryApiClientContext{ 188 | Name: "MCP Toolbox", 189 | }, 190 | } 191 | json, err := json.Marshal(req2) 192 | if err != nil { 193 | t.Fatalf("Could not marshall req2 as json") 194 | } 195 | got := string(json) 196 | want := `{"query":{"model":"model","view":"explore","fields":["foo.bar"]},"render_options":{"format":"json"},"query_api_client_context":{"name":"MCP Toolbox"}}` 197 | if diff := cmp.Diff(want, got); diff != "" { 198 | t.Fatalf("incorrect result: diff %v", diff) 199 | } 200 | 201 | } 202 | ``` -------------------------------------------------------------------------------- /tests/yugabytedb/yugabytedb_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yugabytedb 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "regexp" 22 | "strings" 23 | "testing" 24 | "time" 25 | 26 | "github.com/google/uuid" 27 | "github.com/googleapis/genai-toolbox/internal/testutils" 28 | "github.com/googleapis/genai-toolbox/tests" 29 | "github.com/yugabyte/pgx/v5/pgxpool" 30 | ) 31 | 32 | var ( 33 | YBDB_SOURCE_KIND = "yugabytedb" 34 | YBDB_TOOL_KIND = "yugabytedb-sql" 35 | YBDB_DATABASE = os.Getenv("YUGABYTEDB_DATABASE") 36 | YBDB_HOST = os.Getenv("YUGABYTEDB_HOST") 37 | YBDB_PORT = os.Getenv("YUGABYTEDB_PORT") 38 | YBDB_USER = os.Getenv("YUGABYTEDB_USER") 39 | YBDB_PASS = os.Getenv("YUGABYTEDB_PASS") 40 | YBDB_LB = os.Getenv("YUGABYTEDB_LOADBALANCE") 41 | ) 42 | 43 | func getYBVars(t *testing.T) map[string]any { 44 | switch "" { 45 | case YBDB_DATABASE: 46 | t.Fatal("'YUGABYTEDB_DATABASE' not set") 47 | case YBDB_HOST: 48 | t.Fatal("'YUGABYTEDB_HOST' not set") 49 | case YBDB_PORT: 50 | t.Fatal("'YUGABYTEDB_PORT' not set") 51 | case YBDB_USER: 52 | t.Fatal("'YUGABYTEDB_USER' not set") 53 | case YBDB_PASS: 54 | t.Fatal("'YUGABYTEDB_PASS' not set") 55 | case YBDB_LB: 56 | fmt.Printf("YUGABYTEDB_LOADBALANCE value not set. Setting default value: false") 57 | YBDB_LB = "false" 58 | } 59 | 60 | return map[string]any{ 61 | "kind": YBDB_SOURCE_KIND, 62 | "host": YBDB_HOST, 63 | "port": YBDB_PORT, 64 | "database": YBDB_DATABASE, 65 | "user": YBDB_USER, 66 | "password": YBDB_PASS, 67 | "loadBalance": YBDB_LB, 68 | } 69 | } 70 | 71 | func initYBConnectionPool(host, port, user, pass, dbname, loadBalance string) (*pgxpool.Pool, error) { 72 | dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?load_balance=%s", user, pass, host, port, dbname, loadBalance) 73 | pool, err := pgxpool.New(context.Background(), dsn) 74 | if err != nil { 75 | return nil, fmt.Errorf("unable to create YugabyteDB connection pool: %w", err) 76 | } 77 | return pool, nil 78 | } 79 | 80 | // SetupYugabyteDBSQLTable creates and inserts data into a table of tool 81 | // compatible with yugabytedb-sql tool 82 | func SetupYugabyteDBSQLTable(t *testing.T, ctx context.Context, pool *pgxpool.Pool, create_statement, insert_statement, tableName string, params []any) func(*testing.T) { 83 | err := pool.Ping(ctx) 84 | if err != nil { 85 | t.Fatalf("unable to connect to test database: %s", err) 86 | } 87 | 88 | // Create table 89 | _, err = pool.Query(ctx, create_statement) 90 | if err != nil { 91 | t.Fatalf("unable to create test table %s: %s", tableName, err) 92 | } 93 | 94 | // Insert test data 95 | _, err = pool.Query(ctx, insert_statement, params...) 96 | if err != nil { 97 | t.Fatalf("unable to insert test data: %s", err) 98 | } 99 | 100 | return func(t *testing.T) { 101 | // tear down test 102 | _, err = pool.Exec(ctx, fmt.Sprintf("DROP TABLE %s;", tableName)) 103 | if err != nil { 104 | t.Errorf("Teardown failed: %s", err) 105 | } 106 | } 107 | } 108 | 109 | func TestYugabyteDB(t *testing.T) { 110 | sourceConfig := getYBVars(t) 111 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 112 | defer cancel() 113 | 114 | var args []string 115 | 116 | pool, err := initYBConnectionPool(YBDB_HOST, YBDB_PORT, YBDB_USER, YBDB_PASS, YBDB_DATABASE, YBDB_LB) 117 | if err != nil { 118 | t.Fatalf("unable to create YugabyteDB connection pool: %s", err) 119 | } 120 | 121 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 122 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 123 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 124 | 125 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam) 126 | teardownTable1 := SetupYugabyteDBSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 127 | defer teardownTable1(t) 128 | 129 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetPostgresSQLAuthToolInfo(tableNameAuth) 130 | teardownTable2 := SetupYugabyteDBSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 131 | defer teardownTable2(t) 132 | 133 | toolsFile := tests.GetToolsConfig(sourceConfig, YBDB_TOOL_KIND, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 134 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement() 135 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, YBDB_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "") 136 | 137 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 138 | if err != nil { 139 | t.Fatalf("command initialization returned an error: %s", err) 140 | } 141 | defer cleanup() 142 | 143 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 144 | defer cancel() 145 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 146 | if err != nil { 147 | t.Logf("toolbox command logs: \n%s", out) 148 | t.Fatalf("toolbox didn't start successfully: %s", err) 149 | } 150 | 151 | select1Want, mcpMyFailToolWant, _, mcpSelect1Want := tests.GetPostgresWants() 152 | 153 | tests.RunToolGetTest(t) 154 | tests.RunToolInvokeTest(t, select1Want) 155 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 156 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 157 | } 158 | ```