This is page 13 of 33. Use http://codebase.md/googleapis/genai-toolbox?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ └── looker-run-look.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ └── lookerrunlook │ │ │ ├── lookerrunlook_test.go │ │ │ └── lookerrunlook.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /internal/tools/clickhouse/clickhousesql/clickhousesql.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package clickhouse import ( "context" "database/sql" "fmt" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/tools" ) type compatibleSource interface { ClickHousePool() *sql.DB } var compatibleSources = []string{"clickhouse"} const sqlKind string = "clickhouse-sql" func init() { if !tools.Register(sqlKind, newSQLConfig) { panic(fmt.Sprintf("tool kind %q already registered", sqlKind)) } } func newSQLConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` Statement string `yaml:"statement" validate:"required"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` TemplateParameters tools.Parameters `yaml:"templateParameters"` } var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return sqlKind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", sqlKind, compatibleSources) } allParameters, paramManifest, _ := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) t := Tool{ Name: cfg.Name, Kind: sqlKind, Parameters: cfg.Parameters, TemplateParameters: cfg.TemplateParameters, AllParams: allParameters, Statement: cfg.Statement, AuthRequired: cfg.AuthRequired, Pool: s.ClickHousePool(), manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` TemplateParameters tools.Parameters `yaml:"templateParameters"` AllParams tools.Parameters `yaml:"allParams"` Pool *sql.DB Statement string manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, token tools.AccessToken) (any, error) { paramsMap := params.AsMap() newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) if err != nil { return nil, fmt.Errorf("unable to extract template params: %w", err) } newParams, err := tools.GetParams(t.Parameters, paramsMap) if err != nil { return nil, fmt.Errorf("unable to extract standard params: %w", err) } sliceParams := newParams.AsSlice() results, err := t.Pool.QueryContext(ctx, newStatement, sliceParams...) if err != nil { return nil, fmt.Errorf("unable to execute query: %w", err) } cols, err := results.Columns() if err != nil { return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) } rawValues := make([]any, len(cols)) values := make([]any, len(cols)) for i := range rawValues { values[i] = &rawValues[i] } colTypes, err := results.ColumnTypes() if err != nil { return nil, fmt.Errorf("unable to get column types: %w", err) } var out []any for results.Next() { err := results.Scan(values...) if err != nil { return nil, fmt.Errorf("unable to parse row: %w", err) } vMap := make(map[string]any) for i, name := range cols { switch colTypes[i].DatabaseTypeName() { case "String", "FixedString": if rawValues[i] != nil { // Handle potential []byte to string conversion if needed if b, ok := rawValues[i].([]byte); ok { vMap[name] = string(b) } else { vMap[name] = rawValues[i] } } else { vMap[name] = nil } default: vMap[name] = rawValues[i] } } out = append(out, vMap) } err = results.Close() if err != nil { return nil, fmt.Errorf("unable to close rows: %w", err) } if err := results.Err(); err != nil { return nil, fmt.Errorf("errors encountered by results.Scan: %w", err) } return out, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.AllParams, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/tools/spanner/spannersql/spanner_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package spannersql_test import ( "testing" yaml "github.com/goccy/go-yaml" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/server" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannersql" ) func TestParseFromYamlSpanner(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } tcs := []struct { desc string in string want server.ToolConfigs }{ { desc: "basic example", in: ` tools: example_tool: kind: spanner-sql source: my-pg-instance description: some description statement: | SELECT * FROM SQL_STATEMENT; parameters: - name: country type: string description: some description `, want: server.ToolConfigs{ "example_tool": spannersql.Config{ Name: "example_tool", Kind: "spanner-sql", Source: "my-pg-instance", Description: "some description", Statement: "SELECT * FROM SQL_STATEMENT;\n", AuthRequired: []string{}, Parameters: []tools.Parameter{ tools.NewStringParameter("country", "some description"), }, }, }, }, { desc: "read only set to true", in: ` tools: example_tool: kind: spanner-sql source: my-pg-instance description: some description readOnly: true statement: | SELECT * FROM SQL_STATEMENT; parameters: - name: country type: string description: some description `, want: server.ToolConfigs{ "example_tool": spannersql.Config{ Name: "example_tool", Kind: "spanner-sql", Source: "my-pg-instance", Description: "some description", Statement: "SELECT * FROM SQL_STATEMENT;\n", ReadOnly: true, AuthRequired: []string{}, Parameters: []tools.Parameter{ tools.NewStringParameter("country", "some description"), }, }, }, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Tools server.ToolConfigs `yaml:"tools"` }{} // Parse contents err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if diff := cmp.Diff(tc.want, got.Tools); diff != "" { t.Fatalf("incorrect parse: diff %v", diff) } }) } } func TestParseFromYamlWithTemplateParamsSpanner(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } tcs := []struct { desc string in string want server.ToolConfigs }{ { desc: "basic example", in: ` tools: example_tool: kind: spanner-sql source: my-pg-instance description: some description statement: | SELECT * FROM SQL_STATEMENT; parameters: - name: country type: string description: some description templateParameters: - name: tableName type: string description: The table to select hotels from. - name: fieldArray type: array description: The columns to return for the query. items: name: column type: string description: A column name that will be returned from the query. `, want: server.ToolConfigs{ "example_tool": spannersql.Config{ Name: "example_tool", Kind: "spanner-sql", Source: "my-pg-instance", Description: "some description", Statement: "SELECT * FROM SQL_STATEMENT;\n", AuthRequired: []string{}, Parameters: []tools.Parameter{ tools.NewStringParameter("country", "some description"), }, TemplateParameters: []tools.Parameter{ tools.NewStringParameter("tableName", "The table to select hotels from."), tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), }, }, }, }, { desc: "read only set to true", in: ` tools: example_tool: kind: spanner-sql source: my-pg-instance description: some description readOnly: true statement: | SELECT * FROM SQL_STATEMENT; parameters: - name: country type: string description: some description `, want: server.ToolConfigs{ "example_tool": spannersql.Config{ Name: "example_tool", Kind: "spanner-sql", Source: "my-pg-instance", Description: "some description", Statement: "SELECT * FROM SQL_STATEMENT;\n", ReadOnly: true, AuthRequired: []string{}, Parameters: []tools.Parameter{ tools.NewStringParameter("country", "some description"), }, }, }, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Tools server.ToolConfigs `yaml:"tools"` }{} // Parse contents err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if diff := cmp.Diff(tc.want, got.Tools); diff != "" { t.Fatalf("incorrect parse: diff %v", diff) } }) } } ``` -------------------------------------------------------------------------------- /internal/sources/looker/looker.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package looker import ( "context" "fmt" "time" geminidataanalytics "cloud.google.com/go/geminidataanalytics/apiv1beta" "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/util" "go.opentelemetry.io/otel/trace" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "github.com/looker-open-source/sdk-codegen/go/rtl" v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" ) const SourceKind string = "looker" // validate interface var _ sources.SourceConfig = Config{} func init() { if !sources.Register(SourceKind, newConfig) { panic(fmt.Sprintf("source kind %q already registered", SourceKind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { actual := Config{ Name: name, SslVerification: true, Timeout: "600s", UseClientOAuth: false, ShowHiddenModels: true, ShowHiddenExplores: true, ShowHiddenFields: true, Location: "us", } // Default Ssl,timeout, ShowHidden if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` BaseURL string `yaml:"base_url" validate:"required"` ClientId string `yaml:"client_id"` ClientSecret string `yaml:"client_secret"` SslVerification bool `yaml:"verify_ssl"` UseClientOAuth bool `yaml:"use_client_oauth"` Timeout string `yaml:"timeout"` ShowHiddenModels bool `yaml:"show_hidden_models"` ShowHiddenExplores bool `yaml:"show_hidden_explores"` ShowHiddenFields bool `yaml:"show_hidden_fields"` Project string `yaml:"project"` Location string `yaml:"location"` } func (r Config) SourceConfigKind() string { return SourceKind } // Initialize initializes a Looker Source instance. func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { logger, err := util.LoggerFromContext(ctx) if err != nil { return nil, fmt.Errorf("unable to get logger from ctx: %s", err) } userAgent, err := util.UserAgentFromContext(ctx) if err != nil { return nil, err } duration, err := time.ParseDuration(r.Timeout) if err != nil { return nil, fmt.Errorf("unable to parse Timeout string as time.Duration: %s", err) } if !r.SslVerification { logger.WarnContext(ctx, "Insecure HTTP is enabled for Looker source %s. TLS certificate verification is skipped.\n", r.Name) } cfg := rtl.ApiSettings{ AgentTag: userAgent, BaseUrl: r.BaseURL, ApiVersion: "4.0", VerifySsl: r.SslVerification, Timeout: int32(duration.Seconds()), ClientId: r.ClientId, ClientSecret: r.ClientSecret, } var tokenSource oauth2.TokenSource tokenSource, _ = initGoogleCloudConnection(ctx) s := &Source{ Name: r.Name, Kind: SourceKind, Timeout: r.Timeout, UseClientOAuth: r.UseClientOAuth, ApiSettings: &cfg, ShowHiddenModels: r.ShowHiddenModels, ShowHiddenExplores: r.ShowHiddenExplores, ShowHiddenFields: r.ShowHiddenFields, Project: r.Project, Location: r.Location, TokenSource: tokenSource, } if !r.UseClientOAuth { if r.ClientId == "" || r.ClientSecret == "" { return nil, fmt.Errorf("client_id and client_secret need to be specified") } s.Client = v4.NewLookerSDK(rtl.NewAuthSession(cfg)) resp, err := s.Client.Me("", s.ApiSettings) if err != nil { return nil, fmt.Errorf("incorrect settings: %w", err) } logger.DebugContext(ctx, fmt.Sprintf("logged in as %s %s", *resp.FirstName, *resp.LastName)) } return s, nil } var _ sources.Source = &Source{} type Source struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Timeout string `yaml:"timeout"` Client *v4.LookerSDK ApiSettings *rtl.ApiSettings UseClientOAuth bool `yaml:"use_client_oauth"` ShowHiddenModels bool `yaml:"show_hidden_models"` ShowHiddenExplores bool `yaml:"show_hidden_explores"` ShowHiddenFields bool `yaml:"show_hidden_fields"` Project string `yaml:"project"` Location string `yaml:"location"` TokenSource oauth2.TokenSource } func (s *Source) SourceKind() string { return SourceKind } func (s *Source) GetApiSettings() *rtl.ApiSettings { return s.ApiSettings } func (s *Source) UseClientAuthorization() bool { return s.UseClientOAuth } func (s *Source) GoogleCloudProject() string { return s.Project } func (s *Source) GoogleCloudLocation() string { return s.Location } func (s *Source) GoogleCloudTokenSource() oauth2.TokenSource { return s.TokenSource } func (s *Source) GoogleCloudTokenSourceWithScope(ctx context.Context, scope string) (oauth2.TokenSource, error) { return google.DefaultTokenSource(ctx, scope) } func initGoogleCloudConnection(ctx context.Context) (oauth2.TokenSource, error) { cred, err := google.FindDefaultCredentials(ctx, geminidataanalytics.DefaultAuthScopes()...) if err != nil { return nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", geminidataanalytics.DefaultAuthScopes(), err) } return cred.TokenSource, nil } ``` -------------------------------------------------------------------------------- /internal/sources/trino/trino_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package trino import ( "testing" "github.com/goccy/go-yaml" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/server" "github.com/googleapis/genai-toolbox/internal/testutils" ) func TestBuildTrinoDSN(t *testing.T) { tests := []struct { name string host string port string user string password string catalog string schema string queryTimeout string accessToken string kerberosEnabled bool sslEnabled bool want string wantErr bool }{ { name: "basic configuration", host: "localhost", port: "8080", user: "testuser", catalog: "hive", schema: "default", want: "http://testuser@localhost:8080?catalog=hive&schema=default", wantErr: false, }, { name: "with password", host: "localhost", port: "8080", user: "testuser", password: "testpass", catalog: "hive", schema: "default", want: "http://testuser:testpass@localhost:8080?catalog=hive&schema=default", wantErr: false, }, { name: "with SSL", host: "localhost", port: "8443", user: "testuser", catalog: "hive", schema: "default", sslEnabled: true, want: "https://testuser@localhost:8443?catalog=hive&schema=default", wantErr: false, }, { name: "with access token", host: "localhost", port: "8080", user: "testuser", catalog: "hive", schema: "default", accessToken: "jwt-token-here", want: "http://testuser@localhost:8080?accessToken=jwt-token-here&catalog=hive&schema=default", wantErr: false, }, { name: "with kerberos", host: "localhost", port: "8080", user: "testuser", catalog: "hive", schema: "default", kerberosEnabled: true, want: "http://testuser@localhost:8080?KerberosEnabled=true&catalog=hive&schema=default", wantErr: false, }, { name: "with query timeout", host: "localhost", port: "8080", user: "testuser", catalog: "hive", schema: "default", queryTimeout: "30m", want: "http://testuser@localhost:8080?catalog=hive&queryTimeout=30m&schema=default", wantErr: false, }, { name: "anonymous access (empty user)", host: "localhost", port: "8080", catalog: "hive", schema: "default", want: "http://localhost:8080?catalog=hive&schema=default", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := buildTrinoDSN(tt.host, tt.port, tt.user, tt.password, tt.catalog, tt.schema, tt.queryTimeout, tt.accessToken, tt.kerberosEnabled, tt.sslEnabled) if (err != nil) != tt.wantErr { t.Errorf("buildTrinoDSN() error = %v, wantErr %v", err, tt.wantErr) return } if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("buildTrinoDSN() mismatch (-want +got):\n%s", diff) } }) } } func TestParseFromYamlTrino(t *testing.T) { tcs := []struct { desc string in string want server.SourceConfigs }{ { desc: "basic example", in: ` sources: my-trino-instance: kind: trino host: localhost port: "8080" user: testuser catalog: hive schema: default `, want: server.SourceConfigs{ "my-trino-instance": Config{ Name: "my-trino-instance", Kind: SourceKind, Host: "localhost", Port: "8080", User: "testuser", Catalog: "hive", Schema: "default", }, }, }, { desc: "example with optional fields", in: ` sources: my-trino-instance: kind: trino host: localhost port: "8443" user: testuser password: testpass catalog: hive schema: default queryTimeout: "30m" accessToken: "jwt-token-here" kerberosEnabled: true sslEnabled: true `, want: server.SourceConfigs{ "my-trino-instance": Config{ Name: "my-trino-instance", Kind: SourceKind, Host: "localhost", Port: "8443", User: "testuser", Password: "testpass", Catalog: "hive", Schema: "default", QueryTimeout: "30m", AccessToken: "jwt-token-here", KerberosEnabled: true, SSLEnabled: true, }, }, }, { desc: "anonymous access without user", in: ` sources: my-trino-anonymous: kind: trino host: localhost port: "8080" catalog: hive schema: default `, want: server.SourceConfigs{ "my-trino-anonymous": Config{ Name: "my-trino-anonymous", Kind: SourceKind, Host: "localhost", Port: "8080", Catalog: "hive", Schema: "default", }, }, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Sources server.SourceConfigs `yaml:"sources"` }{} // Parse contents err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if !cmp.Equal(tc.want, got.Sources) { t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) } }) } } ``` -------------------------------------------------------------------------------- /internal/tools/valkey/valkey.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package valkey import ( "context" "fmt" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" valkeysrc "github.com/googleapis/genai-toolbox/internal/sources/valkey" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/valkey-io/valkey-go" ) const kind string = "valkey" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { ValkeyClient() valkey.Client } // validate compatible sources are still compatible var _ compatibleSource = &valkeysrc.Source{} var compatibleSources = [...]string{valkeysrc.SourceKind, valkeysrc.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` Commands [][]string `yaml:"commands" validate:"required"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, cfg.Parameters) // finish tool setup t := Tool{ Name: cfg.Name, Kind: kind, Parameters: cfg.Parameters, Commands: cfg.Commands, AuthRequired: cfg.AuthRequired, Client: s.ValkeyClient(), manifest: tools.Manifest{Description: cfg.Description, Parameters: cfg.Parameters.Manifest(), AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` Client valkey.Client Commands [][]string manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { // Replace parameters commands, err := replaceCommandsParams(t.Commands, t.Parameters, params) if err != nil { return nil, fmt.Errorf("error replacing commands' parameters: %s", err) } // Build commands builtCmds := make(valkey.Commands, len(commands)) for i, cmd := range commands { builtCmds[i] = t.Client.B().Arbitrary(cmd...).Build() } if len(builtCmds) == 0 { return nil, fmt.Errorf("no valid commands were built to execute") } // Execute commands responses := t.Client.DoMulti(ctx, builtCmds...) // Parse responses out := make([]any, len(t.Commands)) for i, resp := range responses { if err := resp.Error(); err != nil { // Add error from each command to `errSum` out[i] = fmt.Sprintf("error from executing command at index %d: %s", i, err) continue } val, err := resp.ToAny() if err != nil { out[i] = fmt.Sprintf("error parsing response: %s", err) continue } out[i] = val } return out, nil } // replaceCommandsParams is a helper function to replace parameters in the commands func replaceCommandsParams(commands [][]string, params tools.Parameters, paramValues tools.ParamValues) ([][]string, error) { paramMap := paramValues.AsMapWithDollarPrefix() typeMap := make(map[string]string, len(params)) for _, p := range params { placeholder := "$" + p.GetName() typeMap[placeholder] = p.GetType() } newCommands := make([][]string, len(commands)) for i, cmd := range commands { newCmd := make([]string, 0) for _, part := range cmd { v, ok := paramMap[part] if !ok { // Command part is not a Parameter placeholder newCmd = append(newCmd, part) continue } if typeMap[part] == "array" { for _, item := range v.([]any) { // Nested arrays will only be expanded once // e.g., [A, [B, C]] --> ["A", "[B C]"] newCmd = append(newCmd, fmt.Sprintf("%s", item)) } continue } newCmd = append(newCmd, fmt.Sprintf("%s", v)) } newCommands[i] = newCmd } return newCommands, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/tools/trino/trinosql/trinosql.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package trinosql import ( "context" "database/sql" "fmt" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/sources/trino" "github.com/googleapis/genai-toolbox/internal/tools" ) const kind string = "trino-sql" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { TrinoDB() *sql.DB } // validate compatible sources are still compatible var _ compatibleSource = &trino.Source{} var compatibleSources = [...]string{trino.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` Statement string `yaml:"statement" validate:"required"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` TemplateParameters tools.Parameters `yaml:"templateParameters"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) if err != nil { return nil, fmt.Errorf("unable to process parameters: %w", err) } mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) // finish tool setup t := Tool{ Name: cfg.Name, Kind: kind, Parameters: cfg.Parameters, TemplateParameters: cfg.TemplateParameters, AllParams: allParameters, Statement: cfg.Statement, AuthRequired: cfg.AuthRequired, Db: s.TrinoDB(), manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` TemplateParameters tools.Parameters `yaml:"templateParameters"` AllParams tools.Parameters `yaml:"allParams"` Statement string Db *sql.DB manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { paramsMap := params.AsMap() newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) if err != nil { return nil, fmt.Errorf("unable to extract template params %w", err) } newParams, err := tools.GetParams(t.Parameters, paramsMap) if err != nil { return nil, fmt.Errorf("unable to extract standard params %w", err) } sliceParams := newParams.AsSlice() results, err := t.Db.QueryContext(ctx, newStatement, sliceParams...) if err != nil { return nil, fmt.Errorf("unable to execute query: %w", err) } defer results.Close() cols, err := results.Columns() if err != nil { return nil, fmt.Errorf("unable to retrieve column names: %w", err) } // create an array of values for each column, which can be re-used to scan each row rawValues := make([]any, len(cols)) values := make([]any, len(cols)) for i := range rawValues { values[i] = &rawValues[i] } var out []any for results.Next() { err := results.Scan(values...) if err != nil { return nil, fmt.Errorf("unable to parse row: %w", err) } vMap := make(map[string]any) for i, name := range cols { val := rawValues[i] if val == nil { vMap[name] = nil continue } // Convert byte arrays to strings for text fields if b, ok := val.([]byte); ok { vMap[name] = string(b) } else { vMap[name] = val } } out = append(out, vMap) } if err := results.Err(); err != nil { return nil, fmt.Errorf("errors encountered during row iteration: %w", err) } return out, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.AllParams, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/tools/looker/lookeradddashboardelement/lookeradddashboardelement.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package lookeradddashboardelement import ( "context" "fmt" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" "github.com/googleapis/genai-toolbox/internal/util" "github.com/looker-open-source/sdk-codegen/go/rtl" v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" ) const kind string = "looker-add-dashboard-element" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` AuthRequired []string `yaml:"authRequired"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(*lookersrc.Source) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind) } parameters := lookercommon.GetQueryParameters() dashIdParameter := tools.NewStringParameter("dashboard_id", "The id of the dashboard where this tile will exist") parameters = append(parameters, dashIdParameter) titleParameter := tools.NewStringParameterWithDefault("title", "", "The title of the Dashboard Element") parameters = append(parameters, titleParameter) vizParameter := tools.NewMapParameterWithDefault("vis_config", map[string]any{}, "The visualization config for the query", "", ) parameters = append(parameters, vizParameter) mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) // finish tool setup return Tool{ Name: cfg.Name, Kind: kind, Parameters: parameters, AuthRequired: cfg.AuthRequired, UseClientOAuth: s.UseClientOAuth, Client: s.Client, ApiSettings: s.ApiSettings, manifest: tools.Manifest{ Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired, }, mcpManifest: mcpManifest, }, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` UseClientOAuth bool Client *v4.LookerSDK ApiSettings *rtl.ApiSettings AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` manifest tools.Manifest mcpManifest tools.McpManifest } var ( dataType string = "data" visType string = "vis" ) func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { logger, err := util.LoggerFromContext(ctx) if err != nil { return nil, fmt.Errorf("unable to get logger from ctx: %s", err) } logger.DebugContext(ctx, "params = ", params) wq, err := lookercommon.ProcessQueryArgs(ctx, params) if err != nil { return nil, fmt.Errorf("error building query request: %w", err) } paramsMap := params.AsMap() dashboard_id := paramsMap["dashboard_id"].(string) title := paramsMap["title"].(string) visConfig := paramsMap["vis_config"].(map[string]any) wq.VisConfig = &visConfig qrespFields := "id" sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken) if err != nil { return nil, fmt.Errorf("error getting sdk: %w", err) } qresp, err := sdk.CreateQuery(*wq, qrespFields, t.ApiSettings) if err != nil { return nil, fmt.Errorf("error making create query request: %w", err) } wde := v4.WriteDashboardElement{ DashboardId: &dashboard_id, Title: &title, QueryId: qresp.Id, } switch len(visConfig) { case 0: wde.Type = &dataType default: wde.Type = &visType } fields := "" req := v4.RequestCreateDashboardElement{ Body: wde, Fields: &fields, } resp, err := sdk.CreateDashboardElement(req, t.ApiSettings) if err != nil { return nil, fmt.Errorf("error making create dashboard element request: %w", err) } logger.DebugContext(ctx, "resp = %v", resp) data := make(map[string]any) data["result"] = fmt.Sprintf("Dashboard element added to dashboard %s", dashboard_id) return data, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return t.UseClientOAuth } ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mongodbaggregate import ( "context" "encoding/json" "fmt" "slices" "github.com/goccy/go-yaml" mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/tools" ) const kind string = "mongodb-aggregate" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` AuthRequired []string `yaml:"authRequired" validate:"required"` Description string `yaml:"description" validate:"required"` Database string `yaml:"database" validate:"required"` Collection string `yaml:"collection" validate:"required"` PipelinePayload string `yaml:"pipelinePayload" validate:"required"` PipelineParams tools.Parameters `yaml:"pipelineParams" validate:"required"` Canonical bool `yaml:"canonical"` ReadOnly bool `yaml:"readOnly"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(*mongosrc.Source) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind) } // Create a slice for all parameters allParameters := slices.Concat(cfg.PipelineParams) // Create Toolbox manifest paramManifest := allParameters.Manifest() if paramManifest == nil { paramManifest = make([]tools.ParameterManifest, 0) } // Create MCP manifest mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) // finish tool setup return Tool{ Name: cfg.Name, Kind: kind, AuthRequired: cfg.AuthRequired, Collection: cfg.Collection, PipelinePayload: cfg.PipelinePayload, PipelineParams: cfg.PipelineParams, Canonical: cfg.Canonical, ReadOnly: cfg.ReadOnly, AllParams: allParameters, database: s.Client.Database(cfg.Database), manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, }, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Description string `yaml:"description"` AuthRequired []string `yaml:"authRequired"` Collection string `yaml:"collection"` PipelinePayload string `yaml:"pipelinePayload"` PipelineParams tools.Parameters `yaml:"pipelineParams"` Canonical bool `yaml:"canonical"` ReadOnly bool `yaml:"readOnly"` AllParams tools.Parameters `yaml:"allParams"` database *mongo.Database manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { paramsMap := params.AsMap() pipelineString, err := tools.PopulateTemplateWithJSON("MongoDBAggregatePipeline", t.PipelinePayload, paramsMap) if err != nil { return nil, fmt.Errorf("error populating pipeline: %s", err) } var pipeline = []bson.M{} err = bson.UnmarshalExtJSON([]byte(pipelineString), t.Canonical, &pipeline) if err != nil { return nil, err } if t.ReadOnly { //fail if we do a merge or an out for _, stage := range pipeline { for key := range stage { if key == "$merge" || key == "$out" { return nil, fmt.Errorf("this is not a read-only pipeline: %+v", stage) } } } } cur, err := t.database.Collection(t.Collection).Aggregate(ctx, pipeline) if err != nil { return nil, err } defer cur.Close(ctx) var data = []any{} err = cur.All(ctx, &data) if err != nil { return nil, err } if len(data) == 0 { return []any{}, nil } var final []any for _, item := range data { tmp, _ := bson.MarshalExtJSON(item, false, false) var tmp2 any err = json.Unmarshal(tmp, &tmp2) if err != nil { return nil, err } final = append(final, tmp2) } return final, err } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.AllParams, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/tools/http/http_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package http_test import ( "strings" "testing" yaml "github.com/goccy/go-yaml" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/server" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/internal/tools" http "github.com/googleapis/genai-toolbox/internal/tools/http" ) func TestParseFromYamlHTTP(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } tcs := []struct { desc string in string want server.ToolConfigs }{ { desc: "basic example", in: ` tools: example_tool: kind: http source: my-instance method: GET description: some description path: search `, want: server.ToolConfigs{ "example_tool": http.Config{ Name: "example_tool", Kind: "http", Source: "my-instance", Method: "GET", Path: "search", Description: "some description", AuthRequired: []string{}, }, }, }, { desc: "advanced example", in: ` tools: example_tool: kind: http source: my-instance method: GET path: "{{.pathParam}}?name=alice&pet=cat" description: some description authRequired: - my-google-auth-service - other-auth-service queryParams: - name: country type: string description: some description authServices: - name: my-google-auth-service field: user_id - name: other-auth-service field: user_id pathParams: - name: pathParam type: string description: path param requestBody: | { "age": {{.age}}, "city": "{{.city}}", "food": {{.food}} } bodyParams: - name: age type: integer description: age num - name: city type: string description: city string headers: Authorization: API_KEY Content-Type: application/json headerParams: - name: Language type: string description: language string `, want: server.ToolConfigs{ "example_tool": http.Config{ Name: "example_tool", Kind: "http", Source: "my-instance", Method: "GET", Path: "{{.pathParam}}?name=alice&pet=cat", Description: "some description", AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, QueryParams: []tools.Parameter{ tools.NewStringParameterWithAuth("country", "some description", []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, {Name: "other-auth-service", Field: "user_id"}}), }, PathParams: tools.Parameters{ &tools.StringParameter{ CommonParameter: tools.CommonParameter{Name: "pathParam", Type: "string", Desc: "path param"}, }, }, RequestBody: `{ "age": {{.age}}, "city": "{{.city}}", "food": {{.food}} } `, BodyParams: []tools.Parameter{tools.NewIntParameter("age", "age num"), tools.NewStringParameter("city", "city string")}, Headers: map[string]string{"Authorization": "API_KEY", "Content-Type": "application/json"}, HeaderParams: []tools.Parameter{tools.NewStringParameter("Language", "language string")}, }, }, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Tools server.ToolConfigs `yaml:"tools"` }{} // Parse contents err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if diff := cmp.Diff(tc.want, got.Tools); diff != "" { t.Fatalf("incorrect parse: diff %v", diff) } }) } } func TestFailParseFromYamlHTTP(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } tcs := []struct { desc string in string err string }{ { desc: "Invalid method", in: ` tools: example_tool: kind: http source: my-instance method: GOT path: "search?name=alice&pet=cat" description: some description authRequired: - my-google-auth-service - other-auth-service queryParams: - name: country type: string description: some description authServices: - name: my-google-auth-service field: user_id - name: other-auth-service field: user_id requestBody: | { "age": {{.age}}, "city": "{{.city}}" } bodyParams: - name: age type: integer description: age num - name: city type: string description: city string headers: Authorization: API_KEY Content-Type: application/json headerParams: - name: Language type: string description: language string `, err: `GOT is not a valid http method`, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Tools server.ToolConfigs `yaml:"tools"` }{} // Parse contents err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) if err == nil { t.Fatalf("expect parsing to fail") } errStr := err.Error() if !strings.Contains(errStr, tc.err) { t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) } }) } } ``` -------------------------------------------------------------------------------- /internal/tools/firestore/firestoregetdocuments/firestoregetdocuments.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package firestoregetdocuments import ( "context" "fmt" firestoreapi "cloud.google.com/go/firestore" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/firestore/util" ) const kind string = "firestore-get-documents" const documentPathsKey string = "documentPaths" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { FirestoreClient() *firestoreapi.Client } // validate compatible sources are still compatible var _ compatibleSource = &firestoreds.Source{} var compatibleSources = [...]string{firestoreds.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` AuthRequired []string `yaml:"authRequired"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to retrieve from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path")) parameters := tools.Parameters{documentPathsParameter} mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) // finish tool setup t := Tool{ Name: cfg.Name, Kind: kind, Parameters: parameters, AuthRequired: cfg.AuthRequired, Client: s.FirestoreClient(), manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` Client *firestoreapi.Client manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { mapParams := params.AsMap() documentPathsRaw, ok := mapParams[documentPathsKey].([]any) if !ok { return nil, fmt.Errorf("invalid or missing '%s' parameter; expected an array", documentPathsKey) } if len(documentPathsRaw) == 0 { return nil, fmt.Errorf("'%s' parameter cannot be empty", documentPathsKey) } // Use ConvertAnySliceToTyped to convert the slice typedSlice, err := tools.ConvertAnySliceToTyped(documentPathsRaw, "string") if err != nil { return nil, fmt.Errorf("failed to convert document paths: %w", err) } documentPaths, ok := typedSlice.([]string) if !ok { return nil, fmt.Errorf("unexpected type conversion error for document paths") } // Validate each document path for i, path := range documentPaths { if err := util.ValidateDocumentPath(path); err != nil { return nil, fmt.Errorf("invalid document path at index %d: %w", i, err) } } // Create document references from paths docRefs := make([]*firestoreapi.DocumentRef, len(documentPaths)) for i, path := range documentPaths { docRefs[i] = t.Client.Doc(path) } // Get all documents snapshots, err := t.Client.GetAll(ctx, docRefs) if err != nil { return nil, fmt.Errorf("failed to get documents: %w", err) } // Convert snapshots to response data results := make([]any, len(snapshots)) for i, snapshot := range snapshots { docData := make(map[string]any) docData["path"] = documentPaths[i] docData["exists"] = snapshot.Exists() if snapshot.Exists() { docData["data"] = snapshot.Data() docData["createTime"] = snapshot.CreateTime docData["updateTime"] = snapshot.UpdateTime docData["readTime"] = snapshot.ReadTime } results[i] = docData } return results, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/sources/tidb/tidb_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tidb_test import ( "testing" yaml "github.com/goccy/go-yaml" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/server" "github.com/googleapis/genai-toolbox/internal/sources/tidb" "github.com/googleapis/genai-toolbox/internal/testutils" ) func TestParseFromYamlTiDB(t *testing.T) { tcs := []struct { desc string in string want server.SourceConfigs }{ { desc: "basic example", in: ` sources: my-tidb-instance: kind: tidb host: 0.0.0.0 port: my-port database: my_db user: my_user password: my_pass `, want: server.SourceConfigs{ "my-tidb-instance": tidb.Config{ Name: "my-tidb-instance", Kind: tidb.SourceKind, Host: "0.0.0.0", Port: "my-port", Database: "my_db", User: "my_user", Password: "my_pass", UseSSL: false, }, }, }, { desc: "with SSL enabled", in: ` sources: my-tidb-cloud: kind: tidb host: gateway01.us-west-2.prod.aws.tidbcloud.com port: 4000 database: test_db user: cloud_user password: cloud_pass ssl: true `, want: server.SourceConfigs{ "my-tidb-cloud": tidb.Config{ Name: "my-tidb-cloud", Kind: tidb.SourceKind, Host: "gateway01.us-west-2.prod.aws.tidbcloud.com", Port: "4000", Database: "test_db", User: "cloud_user", Password: "cloud_pass", UseSSL: true, }, }, }, { desc: "Change SSL enabled due to TiDB Cloud host", in: ` sources: my-tidb-cloud: kind: tidb host: gateway01.us-west-2.prod.aws.tidbcloud.com port: 4000 database: test_db user: cloud_user password: cloud_pass `, want: server.SourceConfigs{ "my-tidb-cloud": tidb.Config{ Name: "my-tidb-cloud", Kind: tidb.SourceKind, Host: "gateway01.us-west-2.prod.aws.tidbcloud.com", Port: "4000", Database: "test_db", User: "cloud_user", Password: "cloud_pass", UseSSL: true, }, }, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Sources server.SourceConfigs `yaml:"sources"` }{} // Parse contents err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if !cmp.Equal(tc.want, got.Sources) { t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) } }) } } func TestFailParseFromYaml(t *testing.T) { tcs := []struct { desc string in string err string }{ { desc: "extra field", in: ` sources: my-tidb-instance: kind: tidb host: 0.0.0.0 port: my-port database: my_db user: my_user password: my_pass ssl: false foo: bar `, err: "unable to parse source \"my-tidb-instance\" as \"tidb\": [2:1] unknown field \"foo\"\n 1 | database: my_db\n> 2 | foo: bar\n ^\n 3 | host: 0.0.0.0\n 4 | kind: tidb\n 5 | password: my_pass\n 6 | ", }, { desc: "missing required field", in: ` sources: my-tidb-instance: kind: tidb port: my-port database: my_db user: my_user password: my_pass ssl: false `, err: "unable to parse source \"my-tidb-instance\" as \"tidb\": Key: 'Config.Host' Error:Field validation for 'Host' failed on the 'required' tag", }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := struct { Sources server.SourceConfigs `yaml:"sources"` }{} // Parse contents err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) if err == nil { t.Fatalf("expect parsing to fail") } errStr := err.Error() if errStr != tc.err { t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) } }) } } func TestIsTiDBCloudHost(t *testing.T) { tcs := []struct { desc string host string want bool }{ { desc: "valid TiDB Cloud host - ap-southeast-1", host: "gateway01.ap-southeast-1.prod.aws.tidbcloud.com", want: true, }, { desc: "invalid TiDB Cloud host - wrong domain", host: "gateway01.ap-southeast-1.prod.aws.tdbcloud.com", want: false, }, { desc: "local IP address", host: "127.0.0.1", want: false, }, { desc: "valid TiDB Cloud host - us-west-2", host: "gateway01.us-west-2.prod.aws.tidbcloud.com", want: true, }, { desc: "valid TiDB Cloud host - dev environment", host: "gateway02.eu-west-1.dev.aws.tidbcloud.com", want: true, }, { desc: "valid TiDB Cloud host - staging environment", host: "gateway03.us-east-1.staging.aws.tidbcloud.com", want: true, }, { desc: "invalid - wrong gateway format", host: "gateway1.us-west-2.prod.aws.tidbcloud.com", want: false, }, { desc: "invalid - missing environment", host: "gateway01.us-west-2.aws.tidbcloud.com", want: false, }, { desc: "invalid - wrong subdomain", host: "gateway01.us-west-2.prod.aws.tidbcloud.org", want: false, }, { desc: "invalid - localhost", host: "localhost", want: false, }, { desc: "invalid - private IP", host: "192.168.1.1", want: false, }, { desc: "invalid - empty string", host: "", want: false, }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { got := tidb.IsTiDBCloudHost(tc.host) if got != tc.want { t.Fatalf("isTiDBCloudHost(%q) = %v, want %v", tc.host, got, tc.want) } }) } } ``` -------------------------------------------------------------------------------- /internal/sources/alloydbpg/alloydb_pg.go: -------------------------------------------------------------------------------- ```go // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package alloydbpg import ( "context" "fmt" "net" "strings" "cloud.google.com/go/alloydbconn" "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/util" "github.com/jackc/pgx/v5/pgxpool" "go.opentelemetry.io/otel/trace" ) const SourceKind string = "alloydb-postgres" // validate interface var _ sources.SourceConfig = Config{} func init() { if !sources.Register(SourceKind, newConfig) { panic(fmt.Sprintf("source kind %q already registered", SourceKind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { actual := Config{Name: name, IPType: "public"} // Default IPType if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Project string `yaml:"project" validate:"required"` Region string `yaml:"region" validate:"required"` Cluster string `yaml:"cluster" validate:"required"` Instance string `yaml:"instance" validate:"required"` IPType sources.IPType `yaml:"ipType" validate:"required"` User string `yaml:"user"` Password string `yaml:"password"` Database string `yaml:"database" validate:"required"` } func (r Config) SourceConfigKind() string { return SourceKind } func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { pool, err := initAlloyDBPgConnectionPool(ctx, tracer, r.Name, r.Project, r.Region, r.Cluster, r.Instance, r.IPType.String(), r.User, r.Password, r.Database) if err != nil { return nil, fmt.Errorf("unable to create pool: %w", err) } err = pool.Ping(ctx) if err != nil { return nil, fmt.Errorf("unable to connect successfully: %w", err) } s := &Source{ Name: r.Name, Kind: SourceKind, Pool: pool, } return s, nil } var _ sources.Source = &Source{} type Source struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Pool *pgxpool.Pool } func (s *Source) SourceKind() string { return SourceKind } func (s *Source) PostgresPool() *pgxpool.Pool { return s.Pool } func getOpts(ipType, userAgent string, useIAM bool) ([]alloydbconn.Option, error) { opts := []alloydbconn.Option{alloydbconn.WithUserAgent(userAgent)} switch strings.ToLower(ipType) { case "private": opts = append(opts, alloydbconn.WithDefaultDialOptions(alloydbconn.WithPrivateIP())) case "public": opts = append(opts, alloydbconn.WithDefaultDialOptions(alloydbconn.WithPublicIP())) case "psc": opts = append(opts, alloydbconn.WithDefaultDialOptions(alloydbconn.WithPSC())) default: return nil, fmt.Errorf("invalid ipType %s", ipType) } if useIAM { opts = append(opts, alloydbconn.WithIAMAuthN()) } return opts, nil } func getConnectionConfig(ctx context.Context, user, pass, dbname string) (string, bool, error) { userAgent, err := util.UserAgentFromContext(ctx) if err != nil { userAgent = "genai-toolbox" } useIAM := true // If username and password both provided, use password authentication if user != "" && pass != "" { dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable application_name=%s", user, pass, dbname, userAgent) useIAM = false return dsn, useIAM, nil } // If username is empty, fetch email from ADC // otherwise, use username as IAM email if user == "" { if pass != "" { // If password is provided without an username, raise an error return "", useIAM, fmt.Errorf("password is provided without a username. Please provide both a username and password, or leave both fields empty") } email, err := sources.GetIAMPrincipalEmailFromADC(ctx) if err != nil { return "", useIAM, fmt.Errorf("error getting email from ADC: %v", err) } user = email } // Construct IAM connection string with username dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable application_name=%s", user, dbname, userAgent) return dsn, useIAM, nil } func initAlloyDBPgConnectionPool(ctx context.Context, tracer trace.Tracer, name, project, region, cluster, instance, ipType, user, pass, dbname string) (*pgxpool.Pool, error) { //nolint:all // Reassigned ctx ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name) defer span.End() dsn, useIAM, err := getConnectionConfig(ctx, user, pass, dbname) if err != nil { return nil, fmt.Errorf("unable to get AlloyDB connection config: %w", err) } config, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, fmt.Errorf("unable to parse connection uri: %w", err) } // Create a new dialer with options userAgent, err := util.UserAgentFromContext(ctx) if err != nil { return nil, err } opts, err := getOpts(ipType, userAgent, useIAM) if err != nil { return nil, err } d, err := alloydbconn.NewDialer(ctx, opts...) if err != nil { return nil, fmt.Errorf("unable to parse connection uri: %w", err) } // Tell the driver to use the AlloyDB Go Connector to create connections i := fmt.Sprintf("projects/%s/locations/%s/clusters/%s/instances/%s", project, region, cluster, instance) config.ConnConfig.DialFunc = func(ctx context.Context, _ string, instance string) (net.Conn, error) { return d.Dial(ctx, i) } // Interact with the driver directly as you normally would pool, err := pgxpool.NewWithConfig(ctx, config) if err != nil { return nil, err } return pool, nil } ``` -------------------------------------------------------------------------------- /internal/tools/bigquery/bigquerylistdatasetids/bigquerylistdatasetids.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package bigquerylistdatasetids import ( "context" "fmt" bigqueryapi "cloud.google.com/go/bigquery" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" bigqueryds "github.com/googleapis/genai-toolbox/internal/sources/bigquery" "github.com/googleapis/genai-toolbox/internal/tools" "google.golang.org/api/iterator" ) const kind string = "bigquery-list-dataset-ids" const projectKey string = "project" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { BigQueryProject() string BigQueryClient() *bigqueryapi.Client BigQueryClientCreator() bigqueryds.BigqueryClientCreator UseClientAuthorization() bool BigQueryAllowedDatasets() []string } // validate compatible sources are still compatible var _ compatibleSource = &bigqueryds.Source{} var compatibleSources = [...]string{bigqueryds.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` AuthRequired []string `yaml:"authRequired"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } var projectParameter tools.Parameter var projectParameterDescription string allowedDatasets := s.BigQueryAllowedDatasets() if len(allowedDatasets) > 0 { projectParameterDescription = "This parameter will be ignored. The list of datasets is restricted to a pre-configured list; No need to provide a project ID." } else { projectParameterDescription = "The Google Cloud project to list dataset ids." } projectParameter = tools.NewStringParameterWithDefault(projectKey, s.BigQueryProject(), projectParameterDescription) parameters := tools.Parameters{projectParameter} mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) // finish tool setup t := Tool{ Name: cfg.Name, Kind: kind, Parameters: parameters, AuthRequired: cfg.AuthRequired, UseClientOAuth: s.UseClientAuthorization(), ClientCreator: s.BigQueryClientCreator(), Client: s.BigQueryClient(), AllowedDatasets: allowedDatasets, manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` UseClientOAuth bool `yaml:"useClientOAuth"` Parameters tools.Parameters `yaml:"parameters"` Client *bigqueryapi.Client ClientCreator bigqueryds.BigqueryClientCreator Statement string AllowedDatasets []string manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { if len(t.AllowedDatasets) > 0 { return t.AllowedDatasets, nil } mapParams := params.AsMap() projectId, ok := mapParams[projectKey].(string) if !ok { return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", projectKey) } bqClient := t.Client // Initialize new client if using user OAuth token if t.UseClientOAuth { tokenStr, err := accessToken.ParseBearerToken() if err != nil { return nil, fmt.Errorf("error parsing access token: %w", err) } bqClient, _, err = t.ClientCreator(tokenStr, false) if err != nil { return nil, fmt.Errorf("error creating client from OAuth access token: %w", err) } } datasetIterator := bqClient.Datasets(ctx) datasetIterator.ProjectID = projectId var datasetIds []any for { dataset, err := datasetIterator.Next() if err == iterator.Done { break } if err != nil { return nil, fmt.Errorf("unable to iterate through datasets: %w", err) } // Remove leading and trailing quotes id := dataset.DatasetID if len(id) >= 2 && id[0] == '"' && id[len(id)-1] == '"' { id = id[1 : len(id)-1] } datasetIds = append(datasetIds, id) } return datasetIds, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return t.UseClientOAuth } ``` -------------------------------------------------------------------------------- /tests/tidb/tidb_integration_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tidb import ( "context" "database/sql" "fmt" "os" "regexp" "strings" "testing" "time" "github.com/google/uuid" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/tests" ) var ( TiDBSourceKind = "tidb" TiDBToolKind = "tidb-sql" TiDBDatabase = os.Getenv("TIDB_DATABASE") TiDBHost = os.Getenv("TIDB_HOST") TiDBPort = os.Getenv("TIDB_PORT") TiDBUser = os.Getenv("TIDB_USER") TiDBPass = os.Getenv("TIDB_PASS") ) func getTiDBVars(t *testing.T) map[string]any { switch "" { case TiDBDatabase: t.Fatal("'TIDB_DATABASE' not set") case TiDBHost: t.Fatal("'TIDB_HOST' not set") case TiDBPort: t.Fatal("'TIDB_PORT' not set") case TiDBUser: t.Fatal("'TIDB_USER' not set") case TiDBPass: t.Fatal("'TIDB_PASS' not set") } return map[string]any{ "kind": TiDBSourceKind, "host": TiDBHost, "port": TiDBPort, "database": TiDBDatabase, "user": TiDBUser, "password": TiDBPass, } } // Copied over from tidb.go func initTiDBConnectionPool(host, port, user, pass, dbname string, useSSL bool) (*sql.DB, error) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4&tls=%t", user, pass, host, port, dbname, useSSL) // Interact with the driver directly as you normally would pool, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("sql.Open: %w", err) } return pool, nil } // getTiDBWants return the expected wants for tidb func getTiDBWants() (string, string, string, string) { select1Want := "[{\"1\":1}]" mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your TiDB version for the right syntax to use line 1 column 5 near \"SELEC 1;\" "}],"isError":true}}` createTableStatement := `"CREATE TABLE t (id SERIAL PRIMARY KEY, name TEXT)"` mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"1\":1}"}]}}` return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want } // addTiDBExecuteSqlConfig gets the tools config for `tidb-execute-sql` func addTiDBExecuteSqlConfig(t *testing.T, config map[string]any) map[string]any { tools, ok := config["tools"].(map[string]any) if !ok { t.Fatalf("unable to get tools from config") } tools["my-exec-sql-tool"] = map[string]any{ "kind": "tidb-execute-sql", "source": "my-instance", "description": "Tool to execute sql", } tools["my-auth-exec-sql-tool"] = map[string]any{ "kind": "tidb-execute-sql", "source": "my-instance", "description": "Tool to execute sql", "authRequired": []string{ "my-google-auth", }, } config["tools"] = tools return config } func TestTiDBToolEndpoints(t *testing.T) { sourceConfig := getTiDBVars(t) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() var args []string pool, err := initTiDBConnectionPool(TiDBHost, TiDBPort, TiDBUser, TiDBPass, TiDBDatabase, false) if err != nil { t.Fatalf("unable to create TiDB connection pool: %s", err) } // create table name with UUID tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") // set up data for param tool createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetMySQLParamToolInfo(tableNameParam) teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) defer teardownTable1(t) // set up data for auth tool createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMySQLAuthToolInfo(tableNameAuth) teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) defer teardownTable2(t) // Write config into a file and pass it to command toolsFile := tests.GetToolsConfig(sourceConfig, TiDBToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) toolsFile = addTiDBExecuteSqlConfig(t, toolsFile) tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement() toolsFile = tests.AddTemplateParamConfig(t, toolsFile, TiDBToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) } defer cleanup() waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) if err != nil { t.Logf("toolbox command logs: \n%s", out) t.Fatalf("toolbox didn't start successfully: %s", err) } // Get configs for tests select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := getTiDBWants() // Run tests tests.RunToolGetTest(t) tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest()) tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) } ``` -------------------------------------------------------------------------------- /internal/tools/postgres/postgreslistactivequeries/postgreslistactivequeries.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package postgreslistactivequeries import ( "context" "fmt" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/sources/alloydbpg" "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg" "github.com/googleapis/genai-toolbox/internal/sources/postgres" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/jackc/pgx/v5/pgxpool" ) const kind string = "postgres-list-active-queries" const listActiveQueriesStatement = ` SELECT pid, usename AS user, datname, application_name, client_addr, state, wait_event_type, wait_event, backend_start, xact_start, query_start, now() - query_start AS query_duration, query FROM pg_stat_activity WHERE state = 'active' AND ($1::INTERVAL IS NULL OR now() - query_start >= $1::INTERVAL) AND ($2::text IS NULL OR application_name NOT IN (SELECT trim(app) FROM unnest(string_to_array($2, ',')) AS app)) ORDER BY query_duration DESC LIMIT COALESCE($3::int, 50); ` func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { PostgresPool() *pgxpool.Pool } // validate compatible sources are still compatible var _ compatibleSource = &alloydbpg.Source{} var _ compatibleSource = &cloudsqlpg.Source{} var _ compatibleSource = &postgres.Source{} var compatibleSources = [...]string{alloydbpg.SourceKind, cloudsqlpg.SourceKind, postgres.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` AuthRequired []string `yaml:"authRequired"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } allParameters := tools.Parameters{ tools.NewStringParameterWithDefault("min_duration", "1 minute", "Optional: Only show queries running at least this long (e.g., '1 minute', '1 second', '2 seconds')."), tools.NewStringParameterWithDefault("exclude_application_names", "", "Optional: A comma-separated list of application names to exclude from the query results. This is useful for filtering out queries from specific applications (e.g., 'psql', 'pgAdmin', 'DBeaver'). The match is case-sensitive. Whitespace around commas and names is automatically handled. If this parameter is omitted, no applications are excluded."), tools.NewIntParameterWithDefault("limit", 50, "Optional: The maximum number of rows to return."), } paramManifest := allParameters.Manifest() mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) // finish tool setup t := Tool{ name: cfg.Name, kind: cfg.Kind, authRequired: cfg.AuthRequired, allParams: allParameters, pool: s.PostgresPool(), manifest: tools.Manifest{ Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired, }, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { name string `yaml:"name"` kind string `yaml:"kind"` authRequired []string `yaml:"authRequired"` allParams tools.Parameters `yaml:"allParams"` pool *pgxpool.Pool manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { paramsMap := params.AsMap() newParams, err := tools.GetParams(t.allParams, paramsMap) if err != nil { return nil, fmt.Errorf("unable to extract standard params %w", err) } sliceParams := newParams.AsSlice() results, err := t.pool.Query(ctx, listActiveQueriesStatement, sliceParams...) if err != nil { return nil, fmt.Errorf("unable to execute query: %w", err) } defer results.Close() fields := results.FieldDescriptions() var out []map[string]any for results.Next() { values, err := results.Values() if err != nil { return nil, fmt.Errorf("unable to parse row: %w", err) } rowMap := make(map[string]any) for i, field := range fields { rowMap[string(field.Name)] = values[i] } out = append(out, rowMap) } return out, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.allParams, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.authRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/telemetry/telemetry.go: -------------------------------------------------------------------------------- ```go // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package telemetry import ( "context" "errors" "fmt" mexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric" texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace" "go.opentelemetry.io/contrib/propagators/autoprop" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.34.0" ) // setupOTelSDK bootstraps the OpenTelemetry pipeline. // If it does not return an error, make sure to call shutdown for proper cleanup. func SetupOTel(ctx context.Context, versionString, telemetryOTLP string, telemetryGCP bool, telemetryServiceName string) (shutdown func(context.Context) error, err error) { var shutdownFuncs []func(context.Context) error // shutdown calls cleanup functions registered via shutdownFuncs. // The errors from the calls are joined. // Each registered cleanup will be invoked once. shutdown = func(ctx context.Context) error { var err error for _, fn := range shutdownFuncs { err = errors.Join(err, fn(ctx)) } shutdownFuncs = nil return err } // handleErr calls shutdown for cleanup and makes sure that all errors are returned. handleErr := func(inErr error) { err = errors.Join(inErr, shutdown(ctx)) } // Configure Context Propagation to use the default W3C traceparent format. otel.SetTextMapPropagator(autoprop.NewTextMapPropagator()) res, err := newResource(ctx, versionString, telemetryServiceName) if err != nil { errMsg := fmt.Errorf("unable to set up resource: %w", err) handleErr(errMsg) return } tracerProvider, err := newTracerProvider(ctx, res, telemetryOTLP, telemetryGCP) if err != nil { errMsg := fmt.Errorf("unable to set up trace provider: %w", err) handleErr(errMsg) return } shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) otel.SetTracerProvider(tracerProvider) meterProvider, err := newMeterProvider(ctx, res, telemetryOTLP, telemetryGCP) if err != nil { errMsg := fmt.Errorf("unable to set up meter provider: %w", err) handleErr(errMsg) return } shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) otel.SetMeterProvider(meterProvider) return shutdown, nil } // newResource create default resources for telemetry data. // Resource represents the entity producing telemetry. func newResource(ctx context.Context, versionString string, telemetryServiceName string) (*resource.Resource, error) { // Ensure default SDK resources and the required service name are set. r, err := resource.New( ctx, resource.WithFromEnv(), // Discover and provide attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables. resource.WithTelemetrySDK(), // Discover and provide information about the OTel SDK used. resource.WithOS(), // Discover and provide OS information. resource.WithContainer(), // Discover and provide container information. resource.WithHost(), //Discover and provide host information. resource.WithSchemaURL(semconv.SchemaURL), // Set the schema url. resource.WithAttributes( // Add other custom resource attributes. semconv.ServiceName(telemetryServiceName), semconv.ServiceVersion(versionString), ), ) if err != nil { return nil, fmt.Errorf("trace provider fail to set up resource: %w", err) } return r, nil } // newTracerProvider creates TracerProvider. // TracerProvider is a factory for Tracers and is responsible for creating spans. func newTracerProvider(ctx context.Context, r *resource.Resource, telemetryOTLP string, telemetryGCP bool) (*tracesdk.TracerProvider, error) { traceOpts := []tracesdk.TracerProviderOption{} if telemetryOTLP != "" { // otlptracehttp provides an OTLP span exporter using HTTP with protobuf payloads. // By default, the telemetry is sent to https://localhost:4318/v1/traces. otlpExporter, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(telemetryOTLP)) if err != nil { return nil, err } traceOpts = append(traceOpts, tracesdk.WithBatcher(otlpExporter)) } if telemetryGCP { gcpExporter, err := texporter.New() if err != nil { return nil, err } traceOpts = append(traceOpts, tracesdk.WithBatcher(gcpExporter)) } traceOpts = append(traceOpts, tracesdk.WithResource(r)) traceProvider := tracesdk.NewTracerProvider(traceOpts...) return traceProvider, nil } // newMeterProvider creates MeterProvider. // MeterProvider is a factory for Meters, and is responsible for creating metrics. func newMeterProvider(ctx context.Context, r *resource.Resource, telemetryOTLP string, telemetryGCP bool) (*metric.MeterProvider, error) { metricOpts := []metric.Option{} if telemetryOTLP != "" { // otlpmetrichttp provides an OTLP metrics exporter using HTTP with protobuf payloads. // By default, the telemetry is sent to https://localhost:4318/v1/metrics. otlpExporter, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithEndpoint(telemetryOTLP)) if err != nil { return nil, err } metricOpts = append(metricOpts, metric.WithReader(metric.NewPeriodicReader(otlpExporter))) } if telemetryGCP { gcpExporter, err := mexporter.New() if err != nil { return nil, err } metricOpts = append(metricOpts, metric.WithReader(metric.NewPeriodicReader(gcpExporter))) } meterProvider := metric.NewMeterProvider(metricOpts...) return meterProvider, nil } ``` -------------------------------------------------------------------------------- /internal/tools/dataplex/dataplexlookupentry/dataplexlookupentry.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dataplexlookupentry import ( "context" "fmt" dataplexapi "cloud.google.com/go/dataplex/apiv1" dataplexpb "cloud.google.com/go/dataplex/apiv1/dataplexpb" "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" dataplexds "github.com/googleapis/genai-toolbox/internal/sources/dataplex" "github.com/googleapis/genai-toolbox/internal/tools" ) const kind string = "dataplex-lookup-entry" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { CatalogClient() *dataplexapi.CatalogClient } // validate compatible sources are still compatible var _ compatibleSource = &dataplexds.Source{} var compatibleSources = [...]string{dataplexds.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // Initialize the search configuration with the provided sources rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } viewDesc := ` ## Argument: view **Type:** Integer **Description:** Specifies the parts of the entry and its aspects to return. **Possible Values:** * 1 (BASIC): Returns entry without aspects. * 2 (FULL): Return all required aspects and the keys of non-required aspects. (Default) * 3 (CUSTOM): Return the entry and aspects requested in aspect_types field (at most 100 aspects). Always use this view when aspect_types is not empty. * 4 (ALL): Return the entry and both required and optional aspects (at most 100 aspects) ` name := tools.NewStringParameter("name", "The project to which the request should be attributed in the following form: projects/{project}/locations/{location}.") view := tools.NewIntParameterWithDefault("view", 2, viewDesc) aspectTypes := tools.NewArrayParameterWithDefault("aspectTypes", []any{}, "Limits the aspects returned to the provided aspect types. It only works when used together with CUSTOM view.", tools.NewStringParameter("aspectType", "The types of aspects to be included in the response in the format `projects/{project}/locations/{location}/aspectTypes/{aspectType}`.")) entry := tools.NewStringParameter("entry", "The resource name of the Entry in the following form: projects/{project}/locations/{location}/entryGroups/{entryGroup}/entries/{entry}.") parameters := tools.Parameters{name, view, aspectTypes, entry} mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) t := Tool{ Name: cfg.Name, Kind: kind, Parameters: parameters, AuthRequired: cfg.AuthRequired, CatalogClient: s.CatalogClient(), manifest: tools.Manifest{ Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired, }, mcpManifest: mcpManifest, } return t, nil } type Tool struct { Name string Kind string Parameters tools.Parameters AuthRequired []string CatalogClient *dataplexapi.CatalogClient manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { paramsMap := params.AsMap() viewMap := map[int]dataplexpb.EntryView{ 1: dataplexpb.EntryView_BASIC, 2: dataplexpb.EntryView_FULL, 3: dataplexpb.EntryView_CUSTOM, 4: dataplexpb.EntryView_ALL, } name, _ := paramsMap["name"].(string) entry, _ := paramsMap["entry"].(string) view, _ := paramsMap["view"].(int) aspectTypeSlice, err := tools.ConvertAnySliceToTyped(paramsMap["aspectTypes"].([]any), "string") if err != nil { return nil, fmt.Errorf("can't convert aspectTypes to array of strings: %s", err) } aspectTypes := aspectTypeSlice.([]string) req := &dataplexpb.LookupEntryRequest{ Name: name, View: viewMap[view], AspectTypes: aspectTypes, Entry: entry, } result, err := t.CatalogClient.LookupEntry(ctx, req) if err != nil { return nil, err } return result, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { // Parse parameters from the provided data return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { // Returns the tool manifest return t.manifest } func (t Tool) McpManifest() tools.McpManifest { // Returns the tool MCP manifest return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/tools/neo4j/neo4jschema/cache/cache.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* Package cache provides a simple, thread-safe, in-memory key-value store. It features item expiration and an optional background process (janitor) that periodically removes expired items. */ package cache import ( "sync" "time" ) const ( // DefaultJanitorInterval is the default interval at which the janitor // runs to clean up expired cache items. DefaultJanitorInterval = 1 * time.Minute // DefaultExpiration is the default time-to-live for a cache item. // Note: This constant is defined but not used in the current implementation, // as expiration is set on a per-item basis. DefaultExpiration = 60 ) // CacheItem represents a value stored in the cache, along with its expiration time. type CacheItem struct { Value any // The actual value being stored. Expiration int64 // The time when the item expires, as a Unix nano timestamp. 0 means no expiration. } // isExpired checks if the cache item has passed its expiration time. // It returns true if the item is expired, and false otherwise. func (item CacheItem) isExpired() bool { // If Expiration is 0, the item is considered to never expire. if item.Expiration == 0 { return false } return time.Now().UnixNano() > item.Expiration } // Cache is a thread-safe, in-memory key-value store with self-cleaning capabilities. type Cache struct { items map[string]CacheItem // The underlying map that stores the cache items. mu sync.RWMutex // A read/write mutex to ensure thread safety for concurrent access. stop chan struct{} // A channel used to signal the janitor goroutine to stop. } // NewCache creates and returns a new Cache instance. // The janitor for cleaning up expired items is not started by default. // Use the WithJanitor method to start the cleanup process. // // Example: // // c := cache.NewCache() // c.Set("myKey", "myValue", 5*time.Minute) func NewCache() *Cache { return &Cache{ items: make(map[string]CacheItem), } } // WithJanitor starts a background goroutine (janitor) that periodically cleans up // expired items from the cache. If a janitor is already running, it will be // stopped and a new one will be started with the specified interval. // // The interval parameter defines how often the janitor should run. If a non-positive // interval is provided, it defaults to DefaultJanitorInterval (1 minute). // // It returns a pointer to the Cache to allow for method chaining. // // Example: // // // Create a cache that cleans itself every 10 minutes. // c := cache.NewCache().WithJanitor(10 * time.Minute) // defer c.Stop() // It's important to stop the janitor when the cache is no longer needed. func (c *Cache) WithJanitor(interval time.Duration) *Cache { c.mu.Lock() defer c.mu.Unlock() if c.stop != nil { // If a janitor is already running, stop it before starting a new one. close(c.stop) } c.stop = make(chan struct{}) // Use the default interval if an invalid one is provided. if interval <= 0 { interval = DefaultJanitorInterval } // Start the janitor in a new goroutine. go c.janitor(interval, c.stop) return c } // Get retrieves an item from the cache by its key. // It returns the item's value and a boolean. The boolean is true if the key // was found and the item has not expired. Otherwise, it is false. // // Example: // // v, found := c.Get("myKey") // if found { // fmt.Printf("Found value: %v\n", v) // } else { // fmt.Println("Key not found or expired.") // } func (c *Cache) Get(key string) (any, bool) { c.mu.RLock() defer c.mu.RUnlock() item, found := c.items[key] // Return false if the item is not found or if it is found but has expired. if !found || item.isExpired() { return nil, false } return item.Value, true } // Set adds an item to the cache, replacing any existing item with the same key. // // The `ttl` (time-to-live) parameter specifies how long the item should remain // in the cache. If `ttl` is positive, the item will expire after that duration. // If `ttl` is zero or negative, the item will never expire. // // Example: // // // Add a key that expires in 5 minutes. // c.Set("sessionToken", "xyz123", 5*time.Minute) // // // Add a key that never expires. // c.Set("appConfig", "configValue", 0) func (c *Cache) Set(key string, value any, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() var expiration int64 // Calculate the expiration time only if ttl is positive. if ttl > 0 { expiration = time.Now().Add(ttl).UnixNano() } c.items[key] = CacheItem{ Value: value, Expiration: expiration, } } // Stop terminates the background janitor goroutine. // It is safe to call Stop even if the janitor was never started or has already // been stopped. This is useful for cleaning up resources. func (c *Cache) Stop() { c.mu.Lock() defer c.mu.Unlock() if c.stop != nil { close(c.stop) c.stop = nil } } // janitor is the background cleanup worker. It runs in a separate goroutine. // It uses a time.Ticker to periodically trigger the deletion of expired items. func (c *Cache) janitor(interval time.Duration, stopCh chan struct{}) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: // Time to clean up expired items. c.deleteExpired() case <-stopCh: // Stop signal received, exit the goroutine. return } } } // deleteExpired scans the cache and removes all items that have expired. // This function acquires a write lock on the cache to ensure safe mutation. func (c *Cache) deleteExpired() { c.mu.Lock() defer c.mu.Unlock() for k, v := range c.items { if v.isExpired() { delete(c.items, k) } } } ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mongodbupdateone import ( "context" "fmt" "slices" "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb" "github.com/googleapis/genai-toolbox/internal/tools" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) const kind string = "mongodb-update-one" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` AuthRequired []string `yaml:"authRequired" validate:"required"` Description string `yaml:"description" validate:"required"` Database string `yaml:"database" validate:"required"` Collection string `yaml:"collection" validate:"required"` FilterPayload string `yaml:"filterPayload" validate:"required"` FilterParams tools.Parameters `yaml:"filterParams" validate:"required"` UpdatePayload string `yaml:"updatePayload" validate:"required"` UpdateParams tools.Parameters `yaml:"updateParams" validate:"required"` Canonical bool `yaml:"canonical" validate:"required"` Upsert bool `yaml:"upsert"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(*mongosrc.Source) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind) } // Create a slice for all parameters allParameters := slices.Concat(cfg.FilterParams, cfg.UpdateParams) // Verify no duplicate parameter names err := tools.CheckDuplicateParameters(allParameters) if err != nil { return nil, err } // Create Toolbox manifest paramManifest := allParameters.Manifest() if paramManifest == nil { paramManifest = make([]tools.ParameterManifest, 0) } // Create MCP manifest mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) // finish tool setup return Tool{ Name: cfg.Name, Kind: kind, AuthRequired: cfg.AuthRequired, Collection: cfg.Collection, FilterPayload: cfg.FilterPayload, FilterParams: cfg.FilterParams, UpdatePayload: cfg.UpdatePayload, UpdateParams: cfg.UpdateParams, Canonical: cfg.Canonical, Upsert: cfg.Upsert, AllParams: allParameters, database: s.Client.Database(cfg.Database), manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, }, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Description string `yaml:"description"` Collection string `yaml:"collection"` FilterPayload string `yaml:"filterPayload" validate:"required"` FilterParams tools.Parameters UpdatePayload string `yaml:"updatePayload" validate:"required"` UpdateParams tools.Parameters AllParams tools.Parameters Canonical bool `yaml:"canonical" validation:"required"` Upsert bool `yaml:"upsert"` database *mongo.Database manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { paramsMap := params.AsMap() filterString, err := tools.PopulateTemplateWithJSON("MongoDBUpdateOneFilter", t.FilterPayload, paramsMap) if err != nil { return nil, fmt.Errorf("error populating filter: %s", err) } var filter = bson.D{} err = bson.UnmarshalExtJSON([]byte(filterString), false, &filter) if err != nil { return nil, fmt.Errorf("unable to unmarshal filter string: %w", err) } updateString, err := tools.PopulateTemplateWithJSON("MongoDBUpdateOne", t.UpdatePayload, paramsMap) if err != nil { return nil, fmt.Errorf("unable to get update: %w", err) } var update = bson.D{} err = bson.UnmarshalExtJSON([]byte(updateString), t.Canonical, &update) if err != nil { return nil, fmt.Errorf("unable to unmarshal update string: %w", err) } res, err := t.database.Collection(t.Collection).UpdateOne(ctx, filter, update, options.Update().SetUpsert(t.Upsert)) if err != nil { return nil, fmt.Errorf("error updating collection: %w", err) } return res.ModifiedCount, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.AllParams, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /internal/tools/firestore/firestoredeletedocuments/firestoredeletedocuments.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package firestoredeletedocuments import ( "context" "fmt" firestoreapi "cloud.google.com/go/firestore" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore" "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/firestore/util" ) const kind string = "firestore-delete-documents" const documentPathsKey string = "documentPaths" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { FirestoreClient() *firestoreapi.Client } // validate compatible sources are still compatible var _ compatibleSource = &firestoreds.Source{} var compatibleSources = [...]string{firestoreds.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` AuthRequired []string `yaml:"authRequired"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to delete from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path")) parameters := tools.Parameters{documentPathsParameter} mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) // finish tool setup t := Tool{ Name: cfg.Name, Kind: kind, Parameters: parameters, AuthRequired: cfg.AuthRequired, Client: s.FirestoreClient(), manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` Client *firestoreapi.Client manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { mapParams := params.AsMap() documentPathsRaw, ok := mapParams[documentPathsKey].([]any) if !ok { return nil, fmt.Errorf("invalid or missing '%s' parameter; expected an array", documentPathsKey) } if len(documentPathsRaw) == 0 { return nil, fmt.Errorf("'%s' parameter cannot be empty", documentPathsKey) } // Use ConvertAnySliceToTyped to convert the slice typedSlice, err := tools.ConvertAnySliceToTyped(documentPathsRaw, "string") if err != nil { return nil, fmt.Errorf("failed to convert document paths: %w", err) } documentPaths, ok := typedSlice.([]string) if !ok { return nil, fmt.Errorf("unexpected type conversion error for document paths") } // Validate each document path for i, path := range documentPaths { if err := util.ValidateDocumentPath(path); err != nil { return nil, fmt.Errorf("invalid document path at index %d: %w", i, err) } } // Create a BulkWriter to handle multiple deletions efficiently bulkWriter := t.Client.BulkWriter(ctx) // Keep track of jobs for each document jobs := make([]*firestoreapi.BulkWriterJob, len(documentPaths)) // Add all delete operations to the BulkWriter for i, path := range documentPaths { docRef := t.Client.Doc(path) job, err := bulkWriter.Delete(docRef) if err != nil { return nil, fmt.Errorf("failed to add delete operation for document %q: %w", path, err) } jobs[i] = job } // End the BulkWriter to execute all operations bulkWriter.End() // Collect results results := make([]any, len(documentPaths)) for i, job := range jobs { docData := make(map[string]any) docData["path"] = documentPaths[i] // Wait for the job to complete and get the result _, err := job.Results() if err != nil { docData["success"] = false docData["error"] = err.Error() } else { docData["success"] = true } results[i] = docData } return results, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } ``` -------------------------------------------------------------------------------- /tests/cloudsql/cloud_sql_create_database_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cloudsql import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "reflect" "regexp" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/tests" ) var ( createDatabaseToolKind = "cloud-sql-create-database" ) type createDatabaseTransport struct { transport http.RoundTripper url *url.URL } func (t *createDatabaseTransport) RoundTrip(req *http.Request) (*http.Response, error) { if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { req.URL.Scheme = t.url.Scheme req.URL.Host = t.url.Host } return t.transport.RoundTrip(req) } type databaseCreateRequest struct { Name string `json:"name"` Project string `json:"project"` Instance string `json:"instance"` } type masterCreateDatabaseHandler struct { t *testing.T } func (h *masterCreateDatabaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.UserAgent(), "genai-toolbox/") { h.t.Errorf("User-Agent header not found") } var body databaseCreateRequest if err := json.NewDecoder(r.Body).Decode(&body); err != nil { h.t.Fatalf("failed to decode request body: %v", err) } var expectedBody databaseCreateRequest var response any var statusCode int switch body.Name { case "test-db": expectedBody = databaseCreateRequest{ Name: "test-db", Project: "p1", Instance: "i1", } response = map[string]any{"name": "op1", "status": "PENDING"} statusCode = http.StatusOK default: http.Error(w, fmt.Sprintf("unhandled database name: %s", body.Name), http.StatusInternalServerError) return } if diff := cmp.Diff(expectedBody, body); diff != "" { h.t.Errorf("unexpected request body (-want +got):\n%s", diff) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func TestCreateDatabaseToolEndpoints(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() handler := &masterCreateDatabaseHandler{t: t} server := httptest.NewServer(handler) defer server.Close() serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("failed to parse server URL: %v", err) } originalTransport := http.DefaultClient.Transport if originalTransport == nil { originalTransport = http.DefaultTransport } http.DefaultClient.Transport = &createDatabaseTransport{ transport: originalTransport, url: serverURL, } t.Cleanup(func() { http.DefaultClient.Transport = originalTransport }) var args []string toolsFile := getCreateDatabaseToolsConfig() cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) } defer cleanup() waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) if err != nil { t.Logf("toolbox command logs: \n%s", out) t.Fatalf("toolbox didn't start successfully: %s", err) } tcs := []struct { name string toolName string body string want string expectError bool errorStatus int }{ { name: "successful database creation", toolName: "create-database", body: `{"project": "p1", "instance": "i1", "name": "test-db"}`, want: `{"name":"op1","status":"PENDING"}`, }, { name: "missing name", toolName: "create-database", body: `{"project": "p1", "instance": "i1"}`, expectError: true, errorStatus: http.StatusBadRequest, }, } for _, tc := range tcs { tc := tc t.Run(tc.name, func(t *testing.T) { api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if tc.expectError { if resp.StatusCode != tc.errorStatus { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) } return } if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) } var result struct { Result string `json:"result"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } var got, want map[string]any if err := json.Unmarshal([]byte(result.Result), &got); err != nil { t.Fatalf("failed to unmarshal result: %v", err) } if err := json.Unmarshal([]byte(tc.want), &want); err != nil { t.Fatalf("failed to unmarshal want: %v", err) } if !reflect.DeepEqual(got, want) { t.Fatalf("unexpected result: got %+v, want %+v", got, want) } }) } } func getCreateDatabaseToolsConfig() map[string]any { return map[string]any{ "sources": map[string]any{ "my-cloud-sql-source": map[string]any{ "kind": "cloud-sql-admin", }, }, "tools": map[string]any{ "create-database": map[string]any{ "kind": createDatabaseToolKind, "source": "my-cloud-sql-source", }, }, } } ``` -------------------------------------------------------------------------------- /docs/en/reference/cli.md: -------------------------------------------------------------------------------- ```markdown --- title: "CLI" type: docs weight: 1 description: > This page describes the `toolbox` command-line options. --- ## Reference | Flag (Short) | Flag (Long) | Description | Default | |--------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------| | `-a` | `--address` | Address of the interface the server will listen on. | `127.0.0.1` | | | `--disable-reload` | Disables dynamic reloading of tools file. | | | `-h` | `--help` | help for toolbox | | | | `--log-level` | Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'. | `info` | | | `--logging-format` | Specify logging format to use. Allowed: 'standard' or 'JSON'. | `standard` | | `-p` | `--port` | Port the server will listen on. | `5000` | | | `--prebuilt` | Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. See [Prebuilt Tools Reference](prebuilt-tools.md) for allowed values. | | | | `--stdio` | Listens via MCP STDIO instead of acting as a remote HTTP server. | | | | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | | | | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | | | | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` | | | `--tools-file` | File path specifying the tool configuration. Cannot be used with --prebuilt, --tools-files, or --tools-folder. | | | | `--tools-files` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --prebuilt, --tools-file, or --tools-folder. | | | | `--tools-folder` | Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --prebuilt, --tools-file, or --tools-files. | | | | `--ui` | Launches the Toolbox UI web server. | | | `-v` | `--version` | version for toolbox | | ## Examples ### Transport Configuration **Server Settings:** - `--address`, `-a`: Server listening address (default: "127.0.0.1") - `--port`, `-p`: Server listening port (default: 5000) **STDIO:** - `--stdio`: Run in MCP STDIO mode instead of HTTP server #### Usage Examples ```bash # Basic server with custom port configuration ./toolbox --tools-file "tools.yaml" --port 8080 ``` ### Tool Configuration Sources The CLI supports multiple mutually exclusive ways to specify tool configurations: **Single File:** (default) - `--tools-file`: Path to a single YAML configuration file (default: `tools.yaml`) **Multiple Files:** - `--tools-files`: Comma-separated list of YAML files to merge **Directory:** - `--tools-folder`: Directory containing YAML files to load and merge **Prebuilt Configurations:** - `--prebuilt`: Use predefined configurations for specific database types (e.g., 'bigquery', 'postgres', 'spanner'). See [Prebuilt Tools Reference](prebuilt-tools.md) for allowed values. {{< notice tip >}} The CLI enforces mutual exclusivity between configuration source flags, preventing simultaneous use of `--prebuilt` with file-based options, and ensuring only one of `--tools-file`, `--tools-files`, or `--tools-folder` is used at a time. {{< /notice >}} ### Hot Reload Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag. ### Toolbox UI To launch Toolbox's interactive UI, use the `--ui` flag. This allows you to test tools and toolsets with features such as authorized parameters. To learn more, visit [Toolbox UI](../how-to/toolbox-ui/index.md). ``` -------------------------------------------------------------------------------- /internal/tools/redis/redis.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package redis import ( "context" "fmt" yaml "github.com/goccy/go-yaml" "github.com/googleapis/genai-toolbox/internal/sources" redissrc "github.com/googleapis/genai-toolbox/internal/sources/redis" "github.com/googleapis/genai-toolbox/internal/tools" jsoniter "github.com/json-iterator/go" "github.com/redis/go-redis/v9" ) const kind string = "redis" func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) } } func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { actual := Config{Name: name} if err := decoder.DecodeContext(ctx, &actual); err != nil { return nil, err } return actual, nil } type compatibleSource interface { RedisClient() redissrc.RedisClient } // validate compatible sources are still compatible var _ compatibleSource = &redissrc.Source{} var compatibleSources = [...]string{redissrc.SourceKind} type Config struct { Name string `yaml:"name" validate:"required"` Kind string `yaml:"kind" validate:"required"` Source string `yaml:"source" validate:"required"` Description string `yaml:"description" validate:"required"` Commands [][]string `yaml:"commands" validate:"required"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` } // validate interface var _ tools.ToolConfig = Config{} func (cfg Config) ToolConfigKind() string { return kind } func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // verify source exists rawS, ok := srcs[cfg.Source] if !ok { return nil, fmt.Errorf("no source named %q configured", cfg.Source) } // verify the source is compatible s, ok := rawS.(compatibleSource) if !ok { return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) } mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, cfg.Parameters) // finish tool setup t := Tool{ Name: cfg.Name, Kind: kind, Parameters: cfg.Parameters, Commands: cfg.Commands, AuthRequired: cfg.AuthRequired, Client: s.RedisClient(), manifest: tools.Manifest{Description: cfg.Description, Parameters: cfg.Parameters.Manifest(), AuthRequired: cfg.AuthRequired}, mcpManifest: mcpManifest, } return t, nil } // validate interface var _ tools.Tool = Tool{} type Tool struct { Name string `yaml:"name"` Kind string `yaml:"kind"` AuthRequired []string `yaml:"authRequired"` Parameters tools.Parameters `yaml:"parameters"` Client redissrc.RedisClient Commands [][]string manifest tools.Manifest mcpManifest tools.McpManifest } func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { cmds, err := replaceCommandsParams(t.Commands, t.Parameters, params) if err != nil { return nil, fmt.Errorf("error replacing commands' parameters: %s", err) } // Execute commands responses := make([]*redis.Cmd, len(cmds)) for i, cmd := range cmds { responses[i] = t.Client.Do(ctx, cmd...) } // Parse responses out := make([]any, len(t.Commands)) for i, resp := range responses { if err := resp.Err(); err != nil { // Add error from each command to `errSum` errString := fmt.Sprintf("error from executing command at index %d: %s", i, err) out[i] = errString continue } val, err := resp.Result() if err != nil { return nil, fmt.Errorf("error getting result: %s", err) } // If result is a map, convert map[any]any to map[string]any // Because the Go's built-in json/encoding marshalling doesn't support // map[any]any as an input var strMap map[string]any var json = jsoniter.ConfigCompatibleWithStandardLibrary mapStr, err := json.Marshal(val) if err != nil { return nil, fmt.Errorf("error marshalling result: %s", err) } err = json.Unmarshal(mapStr, &strMap) if err != nil { // result is not a map out[i] = val continue } out[i] = strMap } return out, nil } func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { return tools.ParseParams(t.Parameters, data, claims) } func (t Tool) Manifest() tools.Manifest { return t.manifest } func (t Tool) McpManifest() tools.McpManifest { return t.mcpManifest } func (t Tool) Authorized(verifiedAuthServices []string) bool { return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) } func (t Tool) RequiresClientAuthorization() bool { return false } // replaceCommandsParams is a helper function to replace parameters in the commands func replaceCommandsParams(commands [][]string, params tools.Parameters, paramValues tools.ParamValues) ([][]any, error) { paramMap := paramValues.AsMapWithDollarPrefix() typeMap := make(map[string]string, len(params)) for _, p := range params { placeholder := "$" + p.GetName() typeMap[placeholder] = p.GetType() } newCommands := make([][]any, len(commands)) for i, cmd := range commands { newCmd := make([]any, 0) for _, part := range cmd { v, ok := paramMap[part] if !ok { // Command part is not a Parameter placeholder newCmd = append(newCmd, part) continue } if typeMap[part] == "array" { for _, item := range v.([]any) { // Nested arrays will only be expanded once // e.g., [A, [B, C]] --> ["A", "[B C]"] newCmd = append(newCmd, fmt.Sprintf("%s", item)) } continue } newCmd = append(newCmd, fmt.Sprintf("%s", v)) } newCommands[i] = newCmd } return newCommands, nil } ```