This is page 17 of 47. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-create-project-file.md │ │ │ ├── looker-delete-project-file.md │ │ │ ├── looker-dev-mode.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-get-project-file.md │ │ │ ├── looker-get-project-files.md │ │ │ ├── looker-get-projects.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ ├── looker-run-look.md │ │ │ └── looker-update-project-file.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookercreateprojectfile │ │ │ │ ├── lookercreateprojectfile_test.go │ │ │ │ └── lookercreateprojectfile.go │ │ │ ├── lookerdeleteprojectfile │ │ │ │ ├── lookerdeleteprojectfile_test.go │ │ │ │ └── lookerdeleteprojectfile.go │ │ │ ├── lookerdevmode │ │ │ │ ├── lookerdevmode_test.go │ │ │ │ └── lookerdevmode.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookergetprojectfile │ │ │ │ ├── lookergetprojectfile_test.go │ │ │ │ └── lookergetprojectfile.go │ │ │ ├── lookergetprojectfiles │ │ │ │ ├── lookergetprojectfiles_test.go │ │ │ │ └── lookergetprojectfiles.go │ │ │ ├── lookergetprojects │ │ │ │ ├── lookergetprojects_test.go │ │ │ │ └── lookergetprojects.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ ├── lookerrunlook │ │ │ │ ├── lookerrunlook_test.go │ │ │ │ └── lookerrunlook.go │ │ │ └── lookerupdateprojectfile │ │ │ ├── lookerupdateprojectfile_test.go │ │ │ └── lookerupdateprojectfile.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /internal/tools/cassandra/cassandracql/cassandracql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cassandracql 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/gocql/gocql" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cassandra" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | ) 27 | 28 | const kind string = "cassandra-cql" 29 | 30 | func init() { 31 | if !tools.Register(kind, newConfig) { 32 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 33 | } 34 | } 35 | 36 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 37 | actual := Config{Name: name} 38 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 39 | return nil, err 40 | } 41 | return actual, nil 42 | } 43 | 44 | type compatibleSource interface { 45 | CassandraSession() *gocql.Session 46 | } 47 | 48 | var _ compatibleSource = &cassandra.Source{} 49 | 50 | var compatibleSources = [...]string{cassandra.SourceKind} 51 | 52 | type Config struct { 53 | Name string `yaml:"name" validate:"required"` 54 | Kind string `yaml:"kind" validate:"required"` 55 | Source string `yaml:"source" validate:"required"` 56 | Description string `yaml:"description" validate:"required"` 57 | Statement string `yaml:"statement" validate:"required"` 58 | AuthRequired []string `yaml:"authRequired"` 59 | Parameters tools.Parameters `yaml:"parameters"` 60 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 61 | } 62 | 63 | // Initialize implements tools.ToolConfig. 64 | func (c Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 65 | // verify source exists 66 | rawS, ok := srcs[c.Source] 67 | if !ok { 68 | return nil, fmt.Errorf("no source named %q configured", c.Source) 69 | } 70 | 71 | // verify the source is compatible 72 | s, ok := rawS.(compatibleSource) 73 | if !ok { 74 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 75 | } 76 | 77 | allParameters, paramManifest, err := tools.ProcessParameters(c.TemplateParameters, c.Parameters) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | mcpManifest := tools.GetMcpManifest(c.Name, c.Description, c.AuthRequired, allParameters) 83 | 84 | t := Tool{ 85 | Name: c.Name, 86 | Kind: kind, 87 | Parameters: c.Parameters, 88 | TemplateParameters: c.TemplateParameters, 89 | AllParams: allParameters, 90 | Statement: c.Statement, 91 | AuthRequired: c.AuthRequired, 92 | Session: s.CassandraSession(), 93 | manifest: tools.Manifest{Description: c.Description, Parameters: paramManifest, AuthRequired: c.AuthRequired}, 94 | mcpManifest: mcpManifest, 95 | } 96 | return t, nil 97 | } 98 | 99 | // ToolConfigKind implements tools.ToolConfig. 100 | func (c Config) ToolConfigKind() string { 101 | return kind 102 | } 103 | 104 | var _ tools.ToolConfig = Config{} 105 | 106 | type Tool struct { 107 | Name string `yaml:"name"` 108 | Kind string `yaml:"kind"` 109 | AuthRequired []string `yaml:"authRequired"` 110 | Parameters tools.Parameters `yaml:"parameters"` 111 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 112 | AllParams tools.Parameters `yaml:"allParams"` 113 | 114 | Session *gocql.Session 115 | Statement string 116 | manifest tools.Manifest 117 | mcpManifest tools.McpManifest 118 | } 119 | 120 | // RequiresClientAuthorization implements tools.Tool. 121 | func (t Tool) RequiresClientAuthorization() bool { 122 | return false 123 | } 124 | 125 | // Authorized implements tools.Tool. 126 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 127 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 128 | } 129 | 130 | // Invoke implements tools.Tool. 131 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 132 | paramsMap := params.AsMap() 133 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 134 | if err != nil { 135 | return nil, fmt.Errorf("unable to extract template params %w", err) 136 | } 137 | 138 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 139 | if err != nil { 140 | return nil, fmt.Errorf("unable to extract standard params %w", err) 141 | } 142 | sliceParams := newParams.AsSlice() 143 | iter := t.Session.Query(newStatement, sliceParams...).WithContext(ctx).Iter() 144 | 145 | // Create a slice to store the out 146 | var out []map[string]interface{} 147 | 148 | // Scan results into a map and append to the slice 149 | for { 150 | row := make(map[string]interface{}) // Create a new map for each row 151 | if !iter.MapScan(row) { 152 | break // No more rows 153 | } 154 | out = append(out, row) 155 | } 156 | 157 | if err := iter.Close(); err != nil { 158 | return nil, fmt.Errorf("unable to parse rows: %w", err) 159 | } 160 | return out, nil 161 | } 162 | 163 | // Manifest implements tools.Tool. 164 | func (t Tool) Manifest() tools.Manifest { 165 | return t.manifest 166 | } 167 | 168 | // McpManifest implements tools.Tool. 169 | func (t Tool) McpManifest() tools.McpManifest { 170 | return t.mcpManifest 171 | } 172 | 173 | // ParseParams implements tools.Tool. 174 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 175 | return tools.ParseParams(t.AllParams, data, claims) 176 | } 177 | 178 | var _ tools.Tool = Tool{} 179 | ``` -------------------------------------------------------------------------------- /internal/tools/couchbase/couchbase.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package couchbase 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | 22 | "github.com/couchbase/gocb/v2" 23 | yaml "github.com/goccy/go-yaml" 24 | "github.com/googleapis/genai-toolbox/internal/sources" 25 | "github.com/googleapis/genai-toolbox/internal/sources/couchbase" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | ) 28 | 29 | const kind string = "couchbase-sql" 30 | 31 | func init() { 32 | if !tools.Register(kind, newConfig) { 33 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 34 | } 35 | } 36 | 37 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 38 | actual := Config{Name: name} 39 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 40 | return nil, err 41 | } 42 | return actual, nil 43 | } 44 | 45 | type compatibleSource interface { 46 | CouchbaseScope() *gocb.Scope 47 | CouchbaseQueryScanConsistency() uint 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &couchbase.Source{} 52 | 53 | var compatibleSources = [...]string{couchbase.SourceKind} 54 | 55 | type Config struct { 56 | Name string `yaml:"name" validate:"required"` 57 | Kind string `yaml:"kind" validate:"required"` 58 | Source string `yaml:"source" validate:"required"` 59 | Description string `yaml:"description" validate:"required"` 60 | Statement string `yaml:"statement" validate:"required"` 61 | AuthRequired []string `yaml:"authRequired"` 62 | Parameters tools.Parameters `yaml:"parameters"` 63 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 64 | } 65 | 66 | // validate interface 67 | var _ tools.ToolConfig = Config{} 68 | 69 | func (cfg Config) ToolConfigKind() string { 70 | return kind 71 | } 72 | 73 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 74 | // verify source exists 75 | rawS, ok := srcs[cfg.Source] 76 | if !ok { 77 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 78 | } 79 | 80 | // verify the source is compatible 81 | s, ok := rawS.(compatibleSource) 82 | if !ok { 83 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 84 | } 85 | 86 | allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 92 | // finish tool setup 93 | t := Tool{ 94 | Name: cfg.Name, 95 | Kind: kind, 96 | Parameters: cfg.Parameters, 97 | TemplateParameters: cfg.TemplateParameters, 98 | AllParams: allParameters, 99 | Statement: cfg.Statement, 100 | Scope: s.CouchbaseScope(), 101 | QueryScanConsistency: s.CouchbaseQueryScanConsistency(), 102 | AuthRequired: cfg.AuthRequired, 103 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 104 | mcpManifest: mcpManifest, 105 | } 106 | return t, nil 107 | } 108 | 109 | // validate interface 110 | var _ tools.Tool = Tool{} 111 | 112 | type Tool struct { 113 | Name string `yaml:"name"` 114 | Kind string `yaml:"kind"` 115 | Parameters tools.Parameters `yaml:"parameters"` 116 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 117 | AllParams tools.Parameters `yaml:"allParams"` 118 | AuthRequired []string `yaml:"authRequired"` 119 | 120 | Scope *gocb.Scope 121 | QueryScanConsistency uint 122 | Statement string 123 | manifest tools.Manifest 124 | mcpManifest tools.McpManifest 125 | } 126 | 127 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 128 | namedParamsMap := params.AsMap() 129 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, namedParamsMap) 130 | if err != nil { 131 | return nil, fmt.Errorf("unable to extract template params %w", err) 132 | } 133 | 134 | newParams, err := tools.GetParams(t.Parameters, namedParamsMap) 135 | if err != nil { 136 | return nil, fmt.Errorf("unable to extract standard params %w", err) 137 | } 138 | results, err := t.Scope.Query(newStatement, &gocb.QueryOptions{ 139 | ScanConsistency: gocb.QueryScanConsistency(t.QueryScanConsistency), 140 | NamedParameters: newParams.AsMap(), 141 | }) 142 | if err != nil { 143 | return nil, fmt.Errorf("unable to execute query: %w", err) 144 | } 145 | 146 | var out []any 147 | for results.Next() { 148 | var result json.RawMessage 149 | err := results.Row(&result) 150 | if err != nil { 151 | return nil, fmt.Errorf("error processing row: %w", err) 152 | } 153 | out = append(out, result) 154 | } 155 | return out, nil 156 | } 157 | 158 | func (t Tool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (tools.ParamValues, error) { 159 | return tools.ParseParams(t.AllParams, data, claimsMap) 160 | } 161 | 162 | func (t Tool) Manifest() tools.Manifest { 163 | return t.manifest 164 | } 165 | 166 | func (t Tool) McpManifest() tools.McpManifest { 167 | return t.mcpManifest 168 | } 169 | 170 | func (t Tool) Authorized(verifiedAuthSources []string) bool { 171 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthSources) 172 | } 173 | 174 | func (t Tool) RequiresClientAuthorization() bool { 175 | return false 176 | } 177 | ``` -------------------------------------------------------------------------------- /docs/en/resources/sources/cloud-sql-mysql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Cloud SQL for MySQL" 3 | linkTitle: "Cloud SQL (MySQL)" 4 | type: docs 5 | weight: 1 6 | description: > 7 | Cloud SQL for MySQL is a fully-managed database service for MySQL. 8 | 9 | --- 10 | 11 | ## About 12 | 13 | [Cloud SQL for MySQL][csql-mysql-docs] is a fully-managed database service 14 | that helps you set up, maintain, manage, and administer your MySQL 15 | relational databases on Google Cloud Platform. 16 | 17 | If you are new to Cloud SQL for MySQL, you can try [creating and connecting 18 | to a database by following these instructions][csql-mysql-quickstart]. 19 | 20 | [csql-mysql-docs]: https://cloud.google.com/sql/docs/mysql 21 | [csql-mysql-quickstart]: https://cloud.google.com/sql/docs/mysql/connect-instance-local-computer 22 | 23 | ## Available Tools 24 | 25 | - [`mysql-sql`](../tools/mysql/mysql-sql.md) 26 | Execute pre-defined prepared SQL queries in Cloud SQL for MySQL. 27 | 28 | - [`mysql-execute-sql`](../tools/mysql/mysql-execute-sql.md) 29 | Run parameterized SQL queries in Cloud SQL for MySQL. 30 | 31 | - [`mysql-list-active-queries`](../tools/mysql/mysql-list-active-queries.md) 32 | List active queries in Cloud SQL for MySQL. 33 | 34 | - [`mysql-list-tables`](../tools/mysql/mysql-list-tables.md) 35 | List tables in a Cloud SQL for MySQL database. 36 | 37 | - [`mysql-list-tables-missing-unique-indexes`](../tools/mysql/mysql-list-tables-missing-unique-indexes.md) 38 | List tables in a Cloud SQL for MySQL database that do not have primary or unique indices. 39 | 40 | - [`mysql-list-table-fragmentation`](../tools/mysql/mysql-list-table-fragmentation.md) 41 | List table fragmentation in Cloud SQL for MySQL tables. 42 | 43 | ### Pre-built Configurations 44 | 45 | - [Cloud SQL for MySQL using 46 | MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/cloud_sql_mysql_mcp/) 47 | Connect your IDE to Cloud SQL for MySQL using Toolbox. 48 | 49 | ## Requirements 50 | 51 | ### IAM Permissions 52 | 53 | By default, this source uses the [Cloud SQL Go Connector][csql-go-conn] to 54 | authorize and establish mTLS connections to your Cloud SQL instance. The Go 55 | connector uses your [Application Default Credentials (ADC)][adc] to authorize 56 | your connection to Cloud SQL. 57 | 58 | In addition to [setting the ADC for your server][set-adc], you need to ensure 59 | the IAM identity has been given the following IAM roles (or corresponding 60 | permissions): 61 | 62 | - `roles/cloudsql.client` 63 | 64 | {{< notice tip >}} 65 | If you are connecting from Compute Engine, make sure your VM 66 | also has the [proper 67 | scope](https://cloud.google.com/compute/docs/access/service-accounts#accesscopesiam) 68 | to connect using the Cloud SQL Admin API. 69 | {{< /notice >}} 70 | 71 | [csql-go-conn]: https://github.com/GoogleCloudPlatform/cloud-sql-go-connector 72 | [adc]: https://cloud.google.com/docs/authentication#adc 73 | [set-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc 74 | 75 | ### Networking 76 | 77 | Cloud SQL supports connecting over both from external networks via the internet 78 | ([public IP][public-ip]), and internal networks ([private IP][private-ip]). 79 | For more information on choosing between the two options, see the Cloud SQL page 80 | [Connection overview][conn-overview]. 81 | 82 | You can configure the `ipType` parameter in your source configuration to 83 | `public` or `private` to match your cluster's configuration. Regardless of which 84 | you choose, all connections use IAM-based authorization and are encrypted with 85 | mTLS. 86 | 87 | [private-ip]: https://cloud.google.com/sql/docs/mysql/configure-private-ip 88 | [public-ip]: https://cloud.google.com/sql/docs/mysql/configure-ip 89 | [conn-overview]: https://cloud.google.com/sql/docs/mysql/connect-overview 90 | 91 | ### Database User 92 | 93 | Currently, this source only uses standard authentication. You will need to [create 94 | a MySQL user][cloud-sql-users] to login to the database with. 95 | 96 | [cloud-sql-users]: https://cloud.google.com/sql/docs/mysql/create-manage-users 97 | 98 | ## Example 99 | 100 | ```yaml 101 | sources: 102 | my-cloud-sql-mysql-source: 103 | kind: cloud-sql-mysql 104 | project: my-project-id 105 | region: us-central1 106 | instance: my-instance 107 | database: my_db 108 | user: ${USER_NAME} 109 | password: ${PASSWORD} 110 | # ipType: "private" 111 | ``` 112 | 113 | {{< notice tip >}} 114 | Use environment variable replacement with the format ${ENV_NAME} 115 | instead of hardcoding your secrets into the configuration file. 116 | {{< /notice >}} 117 | 118 | ## Reference 119 | 120 | | **field** | **type** | **required** | **description** | 121 | |-----------|:--------:|:------------:|------------------------------------------------------------------------------------------------------| 122 | | kind | string | true | Must be "cloud-sql-mysql". | 123 | | project | string | true | Id of the GCP project that the cluster was created in (e.g. "my-project-id"). | 124 | | region | string | true | Name of the GCP region that the cluster was created in (e.g. "us-central1"). | 125 | | instance | string | true | Name of the Cloud SQL instance within the cluster (e.g. "my-instance"). | 126 | | database | string | true | Name of the MySQL database to connect to (e.g. "my_db"). | 127 | | user | string | true | Name of the MySQL user to connect as (e.g. "my-pg-user"). | 128 | | password | string | true | Password of the MySQL user (e.g. "my-password"). | 129 | | ipType | string | false | IP Type of the Cloud SQL instance, must be either `public`, `private`, or `psc`. Default: `public`. | 130 | ``` -------------------------------------------------------------------------------- /docs/en/how-to/connect_via_mcp.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Connect via MCP Client" 3 | type: docs 4 | weight: 1 5 | description: > 6 | How to connect to Toolbox from a MCP Client. 7 | --- 8 | 9 | ## Toolbox SDKs vs Model Context Protocol (MCP) 10 | 11 | Toolbox now supports connections via both the native Toolbox SDKs and via [Model 12 | Context Protocol (MCP)](https://modelcontextprotocol.io/). However, Toolbox has 13 | several features which are not supported in the MCP specification (such as 14 | Authenticated Parameters and Authorized invocation). 15 | 16 | We recommend using the native SDKs over MCP clients to leverage these features. 17 | The native SDKs can be combined with MCP clients in many cases. 18 | 19 | ### Protocol Versions 20 | 21 | Toolbox currently supports the following versions of MCP specification: 22 | 23 | * [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18) 24 | * [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26) 25 | * [2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05) 26 | 27 | ### Toolbox AuthZ/AuthN Not Supported by MCP 28 | 29 | The auth implementation in Toolbox is not supported in MCP's auth specification. 30 | This includes: 31 | 32 | * [Authenticated Parameters](../resources/tools/#authenticated-parameters) 33 | * [Authorized Invocations](../resources/tools/#authorized-invocations) 34 | 35 | ## Connecting to Toolbox with an MCP client 36 | 37 | ### Before you begin 38 | 39 | {{< notice note >}} 40 | MCP is only compatible with Toolbox version 0.3.0 and above. 41 | {{< /notice >}} 42 | 43 | 1. [Install](../getting-started/introduction/#installing-the-server) 44 | Toolbox version 0.3.0+. 45 | 46 | 1. Make sure you've set up and initialized your database. 47 | 48 | 1. [Set up](../getting-started/configure.md) your `tools.yaml` file. 49 | 50 | ### Connecting via Standard Input/Output (stdio) 51 | 52 | Toolbox supports the 53 | [stdio](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio) 54 | transport protocol. Users that wish to use stdio will have to include the 55 | `--stdio` flag when running Toolbox. 56 | 57 | ```bash 58 | ./toolbox --stdio 59 | ``` 60 | 61 | When running with stdio, Toolbox will listen via stdio instead of acting as a 62 | remote HTTP server. Logs will be set to the `warn` level by default. `debug` and 63 | `info` logs are not supported with stdio. 64 | 65 | {{< notice note >}} 66 | Toolbox enables dynamic reloading by default. To disable, use the 67 | `--disable-reload` flag. 68 | {{< /notice >}} 69 | 70 | ### Connecting via HTTP 71 | 72 | Toolbox supports the HTTP transport protocol with and without SSE. 73 | 74 | {{< tabpane text=true >}} {{% tab header="HTTP with SSE (deprecated)" lang="en" %}} 75 | Add the following configuration to your MCP client configuration: 76 | 77 | ```bash 78 | { 79 | "mcpServers": { 80 | "toolbox": { 81 | "type": "sse", 82 | "url": "http://127.0.0.1:5000/mcp/sse", 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | If you would like to connect to a specific toolset, replace `url` with 89 | `"http://127.0.0.1:5000/mcp/{toolset_name}/sse"`. 90 | 91 | HTTP with SSE is only supported in version `2024-11-05` and is currently 92 | deprecated. 93 | {{% /tab %}} {{% tab header="Streamable HTTP" lang="en" %}} 94 | Add the following configuration to your MCP client configuration: 95 | 96 | ```bash 97 | { 98 | "mcpServers": { 99 | "toolbox": { 100 | "type": "http", 101 | "url": "http://127.0.0.1:5000/mcp", 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | If you would like to connect to a specific toolset, replace `url` with 108 | `"http://127.0.0.1:5000/mcp/{toolset_name}"`. 109 | {{% /tab %}} {{< /tabpane >}} 110 | 111 | ### Using the MCP Inspector with Toolbox 112 | 113 | Use MCP [Inspector](https://github.com/modelcontextprotocol/inspector) for 114 | testing and debugging Toolbox server. 115 | 116 | {{< tabpane text=true >}} 117 | {{% tab header="STDIO" lang="en" %}} 118 | 119 | 1. Run Inspector with Toolbox as a subprocess: 120 | 121 | ```bash 122 | npx @modelcontextprotocol/inspector ./toolbox --stdio 123 | ``` 124 | 125 | 1. For `Transport Type` dropdown menu, select `STDIO`. 126 | 127 | 1. In `Command`, make sure that it is set to :`./toolbox` (or the correct path 128 | to where the Toolbox binary is installed). 129 | 130 | 1. In `Arguments`, make sure that it's filled with `--stdio`. 131 | 132 | 1. Click the `Connect` button. It might take awhile to spin up Toolbox. Voila! 133 | You should be able to inspect your toolbox tools! 134 | {{% /tab %}} 135 | {{% tab header="HTTP with SSE (deprecated)" lang="en" %}} 136 | 1. [Run Toolbox](../getting-started/introduction/#running-the-server). 137 | 138 | 1. In a separate terminal, run Inspector directly through `npx`: 139 | 140 | ```bash 141 | npx @modelcontextprotocol/inspector 142 | ``` 143 | 144 | 1. For `Transport Type` dropdown menu, select `SSE`. 145 | 146 | 1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse` to use all tool or 147 | `http//127.0.0.1:5000/mcp/{toolset_name}/sse` to use a specific toolset. 148 | 149 | 1. Click the `Connect` button. Voila! You should be able to inspect your toolbox 150 | tools! 151 | {{% /tab %}} 152 | {{% tab header="Streamable HTTP" lang="en" %}} 153 | 1. [Run Toolbox](../getting-started/introduction/#running-the-server). 154 | 155 | 1. In a separate terminal, run Inspector directly through `npx`: 156 | 157 | ```bash 158 | npx @modelcontextprotocol/inspector 159 | ``` 160 | 161 | 1. For `Transport Type` dropdown menu, select `Streamable HTTP`. 162 | 163 | 1. For `URL`, type in `http://127.0.0.1:5000/mcp` to use all tool or 164 | `http//127.0.0.1:5000/mcp/{toolset_name}` to use a specific toolset. 165 | 166 | 1. Click the `Connect` button. Voila! You should be able to inspect your toolbox 167 | tools! 168 | {{% /tab %}} {{< /tabpane >}} 169 | 170 | ### Tested Clients 171 | 172 | | Client | SSE Works | MCP Config Docs | 173 | |--------|--------|--------| 174 | | Claude Desktop | ✅ | <https://modelcontextprotocol.io/quickstart/user#1-download-claude-for-desktop> | 175 | | MCP Inspector | ✅ | <https://github.com/modelcontextprotocol/inspector> | 176 | | Cursor | ✅ | <https://docs.cursor.com/context/model-context-protocol> | 177 | | Windsurf | ✅ | <https://docs.windsurf.com/windsurf/mcp> | 178 | | VS Code (Insiders) | ✅ | <https://code.visualstudio.com/docs/copilot/chat/mcp-servers> | 179 | ``` -------------------------------------------------------------------------------- /internal/tools/bigquery/bigquerygetdatasetinfo/bigquerygetdatasetinfo.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package bigquerygetdatasetinfo 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | bigqueryapi "cloud.google.com/go/bigquery" 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | bigqueryds "github.com/googleapis/genai-toolbox/internal/sources/bigquery" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | ) 27 | 28 | const kind string = "bigquery-get-dataset-info" 29 | const projectKey string = "project" 30 | const datasetKey string = "dataset" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | BigQueryProject() string 48 | BigQueryClient() *bigqueryapi.Client 49 | BigQueryClientCreator() bigqueryds.BigqueryClientCreator 50 | UseClientAuthorization() bool 51 | } 52 | 53 | // validate compatible sources are still compatible 54 | var _ compatibleSource = &bigqueryds.Source{} 55 | 56 | var compatibleSources = [...]string{bigqueryds.SourceKind} 57 | 58 | type Config struct { 59 | Name string `yaml:"name" validate:"required"` 60 | Kind string `yaml:"kind" validate:"required"` 61 | Source string `yaml:"source" validate:"required"` 62 | Description string `yaml:"description" validate:"required"` 63 | AuthRequired []string `yaml:"authRequired"` 64 | } 65 | 66 | // validate interface 67 | var _ tools.ToolConfig = Config{} 68 | 69 | func (cfg Config) ToolConfigKind() string { 70 | return kind 71 | } 72 | 73 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 74 | // verify source exists 75 | rawS, ok := srcs[cfg.Source] 76 | if !ok { 77 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 78 | } 79 | 80 | // verify the source is compatible 81 | s, ok := rawS.(compatibleSource) 82 | if !ok { 83 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 84 | } 85 | 86 | projectParameter := tools.NewStringParameterWithDefault(projectKey, s.BigQueryProject(), "The Google Cloud project ID containing the dataset.") 87 | datasetParameter := tools.NewStringParameter(datasetKey, "The dataset to get metadata information.") 88 | parameters := tools.Parameters{projectParameter, datasetParameter} 89 | 90 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 91 | 92 | // finish tool setup 93 | t := Tool{ 94 | Name: cfg.Name, 95 | Kind: kind, 96 | Parameters: parameters, 97 | AuthRequired: cfg.AuthRequired, 98 | UseClientOAuth: s.UseClientAuthorization(), 99 | ClientCreator: s.BigQueryClientCreator(), 100 | Client: s.BigQueryClient(), 101 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 102 | mcpManifest: mcpManifest, 103 | } 104 | return t, nil 105 | } 106 | 107 | // validate interface 108 | var _ tools.Tool = Tool{} 109 | 110 | type Tool struct { 111 | Name string `yaml:"name"` 112 | Kind string `yaml:"kind"` 113 | AuthRequired []string `yaml:"authRequired"` 114 | UseClientOAuth bool `yaml:"useClientOAuth"` 115 | Parameters tools.Parameters `yaml:"parameters"` 116 | 117 | Client *bigqueryapi.Client 118 | ClientCreator bigqueryds.BigqueryClientCreator 119 | Statement string 120 | manifest tools.Manifest 121 | mcpManifest tools.McpManifest 122 | } 123 | 124 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 125 | mapParams := params.AsMap() 126 | projectId, ok := mapParams[projectKey].(string) 127 | if !ok { 128 | return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", projectKey) 129 | } 130 | 131 | datasetId, ok := mapParams[datasetKey].(string) 132 | if !ok { 133 | return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", datasetKey) 134 | } 135 | 136 | bqClient := t.Client 137 | var err error 138 | 139 | // Initialize new client if using user OAuth token 140 | if t.UseClientOAuth { 141 | tokenStr, err := accessToken.ParseBearerToken() 142 | if err != nil { 143 | return nil, fmt.Errorf("error parsing access token: %w", err) 144 | } 145 | bqClient, _, err = t.ClientCreator(tokenStr, false) 146 | if err != nil { 147 | return nil, fmt.Errorf("error creating client from OAuth access token: %w", err) 148 | } 149 | } 150 | dsHandle := bqClient.DatasetInProject(projectId, datasetId) 151 | 152 | metadata, err := dsHandle.Metadata(ctx) 153 | if err != nil { 154 | return nil, fmt.Errorf("failed to get metadata for dataset %s (in project %s): %w", datasetId, bqClient.Project(), err) 155 | } 156 | 157 | return metadata, nil 158 | } 159 | 160 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 161 | return tools.ParseParams(t.Parameters, data, claims) 162 | } 163 | 164 | func (t Tool) Manifest() tools.Manifest { 165 | return t.manifest 166 | } 167 | 168 | func (t Tool) McpManifest() tools.McpManifest { 169 | return t.mcpManifest 170 | } 171 | 172 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 173 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 174 | } 175 | 176 | func (t Tool) RequiresClientAuthorization() bool { 177 | return t.UseClientOAuth 178 | } 179 | ``` -------------------------------------------------------------------------------- /internal/tools/postgres/postgressql/postgressql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package postgressql 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | "github.com/googleapis/genai-toolbox/internal/sources/alloydbpg" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg" 25 | "github.com/googleapis/genai-toolbox/internal/sources/postgres" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | "github.com/jackc/pgx/v5/pgxpool" 28 | ) 29 | 30 | const kind string = "postgres-sql" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | PostgresPool() *pgxpool.Pool 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &alloydbpg.Source{} 52 | var _ compatibleSource = &cloudsqlpg.Source{} 53 | var _ compatibleSource = &postgres.Source{} 54 | 55 | var compatibleSources = [...]string{alloydbpg.SourceKind, cloudsqlpg.SourceKind, postgres.SourceKind} 56 | 57 | type Config struct { 58 | Name string `yaml:"name" validate:"required"` 59 | Kind string `yaml:"kind" validate:"required"` 60 | Source string `yaml:"source" validate:"required"` 61 | Description string `yaml:"description" validate:"required"` 62 | Statement string `yaml:"statement" validate:"required"` 63 | AuthRequired []string `yaml:"authRequired"` 64 | Parameters tools.Parameters `yaml:"parameters"` 65 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 66 | } 67 | 68 | // validate interface 69 | var _ tools.ToolConfig = Config{} 70 | 71 | func (cfg Config) ToolConfigKind() string { 72 | return kind 73 | } 74 | 75 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 76 | // verify source exists 77 | rawS, ok := srcs[cfg.Source] 78 | if !ok { 79 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 80 | } 81 | 82 | // verify the source is compatible 83 | s, ok := rawS.(compatibleSource) 84 | if !ok { 85 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 86 | } 87 | 88 | allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters) 94 | 95 | // finish tool setup 96 | t := Tool{ 97 | Name: cfg.Name, 98 | Kind: kind, 99 | Parameters: cfg.Parameters, 100 | TemplateParameters: cfg.TemplateParameters, 101 | AllParams: allParameters, 102 | Statement: cfg.Statement, 103 | AuthRequired: cfg.AuthRequired, 104 | Pool: s.PostgresPool(), 105 | manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, 106 | mcpManifest: mcpManifest, 107 | } 108 | return t, nil 109 | } 110 | 111 | // validate interface 112 | var _ tools.Tool = Tool{} 113 | 114 | type Tool struct { 115 | Name string `yaml:"name"` 116 | Kind string `yaml:"kind"` 117 | AuthRequired []string `yaml:"authRequired"` 118 | Parameters tools.Parameters `yaml:"parameters"` 119 | TemplateParameters tools.Parameters `yaml:"templateParameters"` 120 | AllParams tools.Parameters `yaml:"allParams"` 121 | 122 | Pool *pgxpool.Pool 123 | Statement string 124 | manifest tools.Manifest 125 | mcpManifest tools.McpManifest 126 | } 127 | 128 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 129 | paramsMap := params.AsMap() 130 | newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap) 131 | if err != nil { 132 | return nil, fmt.Errorf("unable to extract template params %w", err) 133 | } 134 | 135 | newParams, err := tools.GetParams(t.Parameters, paramsMap) 136 | if err != nil { 137 | return nil, fmt.Errorf("unable to extract standard params %w", err) 138 | } 139 | sliceParams := newParams.AsSlice() 140 | results, err := t.Pool.Query(ctx, newStatement, sliceParams...) 141 | if err != nil { 142 | return nil, fmt.Errorf("unable to execute query: %w", err) 143 | } 144 | 145 | fields := results.FieldDescriptions() 146 | 147 | var out []any 148 | for results.Next() { 149 | v, err := results.Values() 150 | if err != nil { 151 | return nil, fmt.Errorf("unable to parse row: %w", err) 152 | } 153 | vMap := make(map[string]any) 154 | for i, f := range fields { 155 | vMap[f.Name] = v[i] 156 | } 157 | out = append(out, vMap) 158 | } 159 | 160 | return out, nil 161 | } 162 | 163 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 164 | return tools.ParseParams(t.AllParams, data, claims) 165 | } 166 | 167 | func (t Tool) Manifest() tools.Manifest { 168 | return t.manifest 169 | } 170 | 171 | func (t Tool) McpManifest() tools.McpManifest { 172 | return t.mcpManifest 173 | } 174 | 175 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 176 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 177 | } 178 | 179 | func (t Tool) RequiresClientAuthorization() bool { 180 | return false 181 | } 182 | ``` -------------------------------------------------------------------------------- /internal/tools/firestore/firestorelistcollections/firestorelistcollections.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package firestorelistcollections 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | firestoreapi "cloud.google.com/go/firestore" 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/tools/firestore/util" 27 | ) 28 | 29 | const kind string = "firestore-list-collections" 30 | const parentPathKey string = "parentPath" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | FirestoreClient() *firestoreapi.Client 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &firestoreds.Source{} 52 | 53 | var compatibleSources = [...]string{firestoreds.SourceKind} 54 | 55 | type Config struct { 56 | Name string `yaml:"name" validate:"required"` 57 | Kind string `yaml:"kind" validate:"required"` 58 | Source string `yaml:"source" validate:"required"` 59 | Description string `yaml:"description" validate:"required"` 60 | AuthRequired []string `yaml:"authRequired"` 61 | } 62 | 63 | // validate interface 64 | var _ tools.ToolConfig = Config{} 65 | 66 | func (cfg Config) ToolConfigKind() string { 67 | return kind 68 | } 69 | 70 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 71 | // verify source exists 72 | rawS, ok := srcs[cfg.Source] 73 | if !ok { 74 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 75 | } 76 | 77 | // verify the source is compatible 78 | s, ok := rawS.(compatibleSource) 79 | if !ok { 80 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 81 | } 82 | 83 | emptyString := "" 84 | parentPathParameter := tools.NewStringParameterWithDefault(parentPathKey, emptyString, "Relative parent document path to list subcollections from (e.g., 'users/userId'). If not provided, lists root collections. Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'") 85 | parameters := tools.Parameters{parentPathParameter} 86 | 87 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 88 | 89 | // finish tool setup 90 | t := Tool{ 91 | Name: cfg.Name, 92 | Kind: kind, 93 | Parameters: parameters, 94 | AuthRequired: cfg.AuthRequired, 95 | Client: s.FirestoreClient(), 96 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 97 | mcpManifest: mcpManifest, 98 | } 99 | return t, nil 100 | } 101 | 102 | // validate interface 103 | var _ tools.Tool = Tool{} 104 | 105 | type Tool struct { 106 | Name string `yaml:"name"` 107 | Kind string `yaml:"kind"` 108 | AuthRequired []string `yaml:"authRequired"` 109 | Parameters tools.Parameters `yaml:"parameters"` 110 | 111 | Client *firestoreapi.Client 112 | manifest tools.Manifest 113 | mcpManifest tools.McpManifest 114 | } 115 | 116 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 117 | mapParams := params.AsMap() 118 | 119 | var collectionRefs []*firestoreapi.CollectionRef 120 | var err error 121 | 122 | // Check if parentPath is provided 123 | parentPath, hasParent := mapParams[parentPathKey].(string) 124 | 125 | if hasParent && parentPath != "" { 126 | // Validate parent document path 127 | if err := util.ValidateDocumentPath(parentPath); err != nil { 128 | return nil, fmt.Errorf("invalid parent document path: %w", err) 129 | } 130 | 131 | // List subcollections of the specified document 132 | docRef := t.Client.Doc(parentPath) 133 | collectionRefs, err = docRef.Collections(ctx).GetAll() 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to list subcollections of document %q: %w", parentPath, err) 136 | } 137 | } else { 138 | // List root collections 139 | collectionRefs, err = t.Client.Collections(ctx).GetAll() 140 | if err != nil { 141 | return nil, fmt.Errorf("failed to list root collections: %w", err) 142 | } 143 | } 144 | 145 | // Convert collection references to response data 146 | results := make([]any, len(collectionRefs)) 147 | for i, collRef := range collectionRefs { 148 | collData := make(map[string]any) 149 | collData["id"] = collRef.ID 150 | collData["path"] = collRef.Path 151 | 152 | // If this is a subcollection, include parent information 153 | if collRef.Parent != nil { 154 | collData["parent"] = collRef.Parent.Path 155 | } 156 | 157 | results[i] = collData 158 | } 159 | 160 | return results, nil 161 | } 162 | 163 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 164 | return tools.ParseParams(t.Parameters, data, claims) 165 | } 166 | 167 | func (t Tool) Manifest() tools.Manifest { 168 | return t.manifest 169 | } 170 | 171 | func (t Tool) McpManifest() tools.McpManifest { 172 | return t.mcpManifest 173 | } 174 | 175 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 176 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 177 | } 178 | 179 | func (t Tool) RequiresClientAuthorization() bool { 180 | return false 181 | } 182 | ``` -------------------------------------------------------------------------------- /internal/tools/tidb/tidbexecutesql/tidbexecutesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tidbexecutesql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/tidb" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/util" 27 | ) 28 | 29 | const kind string = "tidb-execute-sql" 30 | 31 | func init() { 32 | if !tools.Register(kind, newConfig) { 33 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 34 | } 35 | } 36 | 37 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 38 | actual := Config{Name: name} 39 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 40 | return nil, err 41 | } 42 | return actual, nil 43 | } 44 | 45 | type compatibleSource interface { 46 | TiDBPool() *sql.DB 47 | } 48 | 49 | // validate compatible sources are still compatible 50 | var _ compatibleSource = &tidb.Source{} 51 | 52 | var compatibleSources = [...]string{tidb.SourceKind} 53 | 54 | type Config struct { 55 | Name string `yaml:"name" validate:"required"` 56 | Kind string `yaml:"kind" validate:"required"` 57 | Source string `yaml:"source" validate:"required"` 58 | Description string `yaml:"description" validate:"required"` 59 | AuthRequired []string `yaml:"authRequired"` 60 | } 61 | 62 | // validate interface 63 | var _ tools.ToolConfig = Config{} 64 | 65 | func (cfg Config) ToolConfigKind() string { 66 | return kind 67 | } 68 | 69 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 70 | // verify source exists 71 | rawS, ok := srcs[cfg.Source] 72 | if !ok { 73 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 74 | } 75 | 76 | // verify the source is compatible 77 | s, ok := rawS.(compatibleSource) 78 | if !ok { 79 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 80 | } 81 | 82 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 83 | parameters := tools.Parameters{sqlParameter} 84 | 85 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 86 | 87 | // finish tool setup 88 | t := Tool{ 89 | Name: cfg.Name, 90 | Kind: kind, 91 | Parameters: parameters, 92 | AuthRequired: cfg.AuthRequired, 93 | Pool: s.TiDBPool(), 94 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 95 | mcpManifest: mcpManifest, 96 | } 97 | return t, nil 98 | } 99 | 100 | // validate interface 101 | var _ tools.Tool = Tool{} 102 | 103 | type Tool struct { 104 | Name string `yaml:"name"` 105 | Kind string `yaml:"kind"` 106 | AuthRequired []string `yaml:"authRequired"` 107 | Parameters tools.Parameters `yaml:"parameters"` 108 | 109 | Pool *sql.DB 110 | manifest tools.Manifest 111 | mcpManifest tools.McpManifest 112 | } 113 | 114 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 115 | paramsMap := params.AsMap() 116 | sql, ok := paramsMap["sql"].(string) 117 | if !ok { 118 | return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"]) 119 | } 120 | 121 | // Log the query executed for debugging. 122 | logger, err := util.LoggerFromContext(ctx) 123 | if err != nil { 124 | return nil, fmt.Errorf("error getting logger: %s", err) 125 | } 126 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql) 127 | 128 | results, err := t.Pool.QueryContext(ctx, sql) 129 | if err != nil { 130 | return nil, fmt.Errorf("unable to execute query: %w", err) 131 | } 132 | defer results.Close() 133 | 134 | cols, err := results.Columns() 135 | if err != nil { 136 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 137 | } 138 | 139 | // create an array of values for each column, which can be re-used to scan each row 140 | rawValues := make([]any, len(cols)) 141 | values := make([]any, len(cols)) 142 | for i := range rawValues { 143 | values[i] = &rawValues[i] 144 | } 145 | 146 | colTypes, err := results.ColumnTypes() 147 | if err != nil { 148 | return nil, fmt.Errorf("unable to get column types: %w", err) 149 | } 150 | 151 | var out []any 152 | for results.Next() { 153 | err := results.Scan(values...) 154 | if err != nil { 155 | return nil, fmt.Errorf("unable to parse row: %w", err) 156 | } 157 | vMap := make(map[string]any) 158 | for i, name := range cols { 159 | val := rawValues[i] 160 | if val == nil { 161 | vMap[name] = nil 162 | continue 163 | } 164 | 165 | // mysql driver return []uint8 type for "TEXT", "VARCHAR", and "NVARCHAR" 166 | // we'll need to cast it back to string 167 | switch colTypes[i].DatabaseTypeName() { 168 | case "TEXT", "VARCHAR", "NVARCHAR": 169 | vMap[name] = string(val.([]byte)) 170 | default: 171 | vMap[name] = val 172 | } 173 | } 174 | out = append(out, vMap) 175 | } 176 | 177 | if err := results.Err(); err != nil { 178 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 179 | } 180 | 181 | return out, nil 182 | } 183 | 184 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 185 | return tools.ParseParams(t.Parameters, data, claims) 186 | } 187 | 188 | func (t Tool) Manifest() tools.Manifest { 189 | return t.manifest 190 | } 191 | 192 | func (t Tool) McpManifest() tools.McpManifest { 193 | return t.mcpManifest 194 | } 195 | 196 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 197 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 198 | } 199 | 200 | func (t Tool) RequiresClientAuthorization() bool { 201 | return false 202 | } 203 | ``` -------------------------------------------------------------------------------- /internal/tools/trino/trinosql/trinosql_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package trinosql_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/trino/trinosql" 26 | ) 27 | 28 | func TestParseFromYamlTrino(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: trino-sql 44 | source: my-trino-instance 45 | description: some description 46 | statement: | 47 | SELECT * FROM catalog.schema.table WHERE id = ?; 48 | authRequired: 49 | - my-google-auth-service 50 | - other-auth-service 51 | parameters: 52 | - name: id 53 | type: string 54 | description: ID to filter by 55 | authServices: 56 | - name: my-google-auth-service 57 | field: user_id 58 | - name: other-auth-service 59 | field: user_id 60 | `, 61 | want: server.ToolConfigs{ 62 | "example_tool": trinosql.Config{ 63 | Name: "example_tool", 64 | Kind: "trino-sql", 65 | Source: "my-trino-instance", 66 | Description: "some description", 67 | Statement: "SELECT * FROM catalog.schema.table WHERE id = ?;\n", 68 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 69 | Parameters: []tools.Parameter{ 70 | tools.NewStringParameterWithAuth("id", "ID to filter by", 71 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 72 | {Name: "other-auth-service", Field: "user_id"}}), 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | for _, tc := range tcs { 79 | t.Run(tc.desc, func(t *testing.T) { 80 | got := struct { 81 | Tools server.ToolConfigs `yaml:"tools"` 82 | }{} 83 | // Parse contents 84 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 85 | if err != nil { 86 | t.Fatalf("unable to unmarshal: %s", err) 87 | } 88 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 89 | t.Fatalf("incorrect parse: diff %v", diff) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func TestParseFromYamlWithTemplateParamsTrino(t *testing.T) { 96 | ctx, err := testutils.ContextWithNewLogger() 97 | if err != nil { 98 | t.Fatalf("unexpected error: %s", err) 99 | } 100 | tcs := []struct { 101 | desc string 102 | in string 103 | want server.ToolConfigs 104 | }{ 105 | { 106 | desc: "basic example", 107 | in: ` 108 | tools: 109 | example_tool: 110 | kind: trino-sql 111 | source: my-trino-instance 112 | description: some description 113 | statement: | 114 | SELECT * FROM {{ .catalog }}.{{ .schema }}.{{ .tableName }} WHERE country = ?; 115 | authRequired: 116 | - my-google-auth-service 117 | - other-auth-service 118 | parameters: 119 | - name: country 120 | type: string 121 | description: some description 122 | authServices: 123 | - name: my-google-auth-service 124 | field: user_id 125 | - name: other-auth-service 126 | field: user_id 127 | templateParameters: 128 | - name: catalog 129 | type: string 130 | description: The catalog to query from. 131 | - name: schema 132 | type: string 133 | description: The schema to query from. 134 | - name: tableName 135 | type: string 136 | description: The table to select data from. 137 | - name: fieldArray 138 | type: array 139 | description: The columns to return for the query. 140 | items: 141 | name: column 142 | type: string 143 | description: A column name that will be returned from the query. 144 | `, 145 | want: server.ToolConfigs{ 146 | "example_tool": trinosql.Config{ 147 | Name: "example_tool", 148 | Kind: "trino-sql", 149 | Source: "my-trino-instance", 150 | Description: "some description", 151 | Statement: "SELECT * FROM {{ .catalog }}.{{ .schema }}.{{ .tableName }} WHERE country = ?;\n", 152 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 153 | Parameters: []tools.Parameter{ 154 | tools.NewStringParameterWithAuth("country", "some description", 155 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 156 | {Name: "other-auth-service", Field: "user_id"}}), 157 | }, 158 | TemplateParameters: []tools.Parameter{ 159 | tools.NewStringParameter("catalog", "The catalog to query from."), 160 | tools.NewStringParameter("schema", "The schema to query from."), 161 | tools.NewStringParameter("tableName", "The table to select data from."), 162 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 163 | }, 164 | }, 165 | }, 166 | }, 167 | } 168 | for _, tc := range tcs { 169 | t.Run(tc.desc, func(t *testing.T) { 170 | got := struct { 171 | Tools server.ToolConfigs `yaml:"tools"` 172 | }{} 173 | // Parse contents 174 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 175 | if err != nil { 176 | t.Fatalf("unable to unmarshal: %s", err) 177 | } 178 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 179 | t.Fatalf("incorrect parse: diff %v", diff) 180 | } 181 | }) 182 | } 183 | } 184 | ``` -------------------------------------------------------------------------------- /internal/tools/spanner/spannerexecutesql/spannerexecutesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerexecutesql 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "cloud.google.com/go/spanner" 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | spannerdb "github.com/googleapis/genai-toolbox/internal/sources/spanner" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/util" 27 | "google.golang.org/api/iterator" 28 | ) 29 | 30 | const kind string = "spanner-execute-sql" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | SpannerClient() *spanner.Client 48 | DatabaseDialect() string 49 | } 50 | 51 | // validate compatible sources are still compatible 52 | var _ compatibleSource = &spannerdb.Source{} 53 | 54 | var compatibleSources = [...]string{spannerdb.SourceKind} 55 | 56 | type Config struct { 57 | Name string `yaml:"name" validate:"required"` 58 | Kind string `yaml:"kind" validate:"required"` 59 | Source string `yaml:"source" validate:"required"` 60 | Description string `yaml:"description" validate:"required"` 61 | AuthRequired []string `yaml:"authRequired"` 62 | ReadOnly bool `yaml:"readOnly"` 63 | } 64 | 65 | // validate interface 66 | var _ tools.ToolConfig = Config{} 67 | 68 | func (cfg Config) ToolConfigKind() string { 69 | return kind 70 | } 71 | 72 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 73 | // verify source exists 74 | rawS, ok := srcs[cfg.Source] 75 | if !ok { 76 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 77 | } 78 | 79 | // verify the source is compatible 80 | s, ok := rawS.(compatibleSource) 81 | if !ok { 82 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 83 | } 84 | 85 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 86 | parameters := tools.Parameters{sqlParameter} 87 | 88 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 89 | 90 | // finish tool setup 91 | t := Tool{ 92 | Name: cfg.Name, 93 | Kind: kind, 94 | Parameters: parameters, 95 | AuthRequired: cfg.AuthRequired, 96 | ReadOnly: cfg.ReadOnly, 97 | Client: s.SpannerClient(), 98 | dialect: s.DatabaseDialect(), 99 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 100 | mcpManifest: mcpManifest, 101 | } 102 | return t, nil 103 | } 104 | 105 | // validate interface 106 | var _ tools.Tool = Tool{} 107 | 108 | type Tool struct { 109 | Name string `yaml:"name"` 110 | Kind string `yaml:"kind"` 111 | AuthRequired []string `yaml:"authRequired"` 112 | Parameters tools.Parameters `yaml:"parameters"` 113 | ReadOnly bool `yaml:"readOnly"` 114 | Client *spanner.Client 115 | dialect string 116 | manifest tools.Manifest 117 | mcpManifest tools.McpManifest 118 | } 119 | 120 | // processRows iterates over the spanner.RowIterator and converts each row to a map[string]any. 121 | func processRows(iter *spanner.RowIterator) ([]any, error) { 122 | var out []any 123 | defer iter.Stop() 124 | 125 | for { 126 | row, err := iter.Next() 127 | if err == iterator.Done { 128 | break 129 | } 130 | if err != nil { 131 | return nil, fmt.Errorf("unable to parse row: %w", err) 132 | } 133 | 134 | vMap := make(map[string]any) 135 | cols := row.ColumnNames() 136 | for i, c := range cols { 137 | vMap[c] = row.ColumnValue(i) 138 | } 139 | out = append(out, vMap) 140 | } 141 | return out, nil 142 | } 143 | 144 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 145 | paramsMap := params.AsMap() 146 | sql, ok := paramsMap["sql"].(string) 147 | if !ok { 148 | return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"]) 149 | } 150 | 151 | // Log the query executed for debugging. 152 | logger, err := util.LoggerFromContext(ctx) 153 | if err != nil { 154 | return nil, fmt.Errorf("error getting logger: %s", err) 155 | } 156 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql) 157 | 158 | var results []any 159 | var opErr error 160 | stmt := spanner.Statement{SQL: sql} 161 | 162 | if t.ReadOnly { 163 | iter := t.Client.Single().Query(ctx, stmt) 164 | results, opErr = processRows(iter) 165 | } else { 166 | _, opErr = t.Client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { 167 | var err error 168 | iter := txn.Query(ctx, stmt) 169 | results, err = processRows(iter) 170 | if err != nil { 171 | return err 172 | } 173 | return nil 174 | }) 175 | } 176 | 177 | if opErr != nil { 178 | return nil, fmt.Errorf("unable to execute query: %w", opErr) 179 | } 180 | 181 | return results, nil 182 | } 183 | 184 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 185 | return tools.ParseParams(t.Parameters, data, claims) 186 | } 187 | 188 | func (t Tool) Manifest() tools.Manifest { 189 | return t.manifest 190 | } 191 | 192 | func (t Tool) McpManifest() tools.McpManifest { 193 | return t.mcpManifest 194 | } 195 | 196 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 197 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 198 | } 199 | 200 | func (t Tool) RequiresClientAuthorization() bool { 201 | return false 202 | } 203 | ``` -------------------------------------------------------------------------------- /internal/tools/mssql/mssqlexecutesql/mssqlexecutesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mssqlexecutesql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmssql" 25 | "github.com/googleapis/genai-toolbox/internal/sources/mssql" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | "github.com/googleapis/genai-toolbox/internal/util" 28 | ) 29 | 30 | const kind string = "mssql-execute-sql" 31 | 32 | func init() { 33 | if !tools.Register(kind, newConfig) { 34 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 35 | } 36 | } 37 | 38 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 39 | actual := Config{Name: name} 40 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 41 | return nil, err 42 | } 43 | return actual, nil 44 | } 45 | 46 | type compatibleSource interface { 47 | MSSQLDB() *sql.DB 48 | } 49 | 50 | // validate compatible sources are still compatible 51 | var _ compatibleSource = &cloudsqlmssql.Source{} 52 | var _ compatibleSource = &mssql.Source{} 53 | 54 | var compatibleSources = [...]string{cloudsqlmssql.SourceKind, mssql.SourceKind} 55 | 56 | type Config struct { 57 | Name string `yaml:"name" validate:"required"` 58 | Kind string `yaml:"kind" validate:"required"` 59 | Source string `yaml:"source" validate:"required"` 60 | Description string `yaml:"description" validate:"required"` 61 | AuthRequired []string `yaml:"authRequired"` 62 | } 63 | 64 | // validate interface 65 | var _ tools.ToolConfig = Config{} 66 | 67 | func (cfg Config) ToolConfigKind() string { 68 | return kind 69 | } 70 | 71 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 72 | // verify source exists 73 | rawS, ok := srcs[cfg.Source] 74 | if !ok { 75 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 76 | } 77 | 78 | // verify the source is compatible 79 | s, ok := rawS.(compatibleSource) 80 | if !ok { 81 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 82 | } 83 | 84 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 85 | parameters := tools.Parameters{sqlParameter} 86 | 87 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 88 | 89 | // finish tool setup 90 | t := Tool{ 91 | Name: cfg.Name, 92 | Kind: kind, 93 | Parameters: parameters, 94 | AuthRequired: cfg.AuthRequired, 95 | Pool: s.MSSQLDB(), 96 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 97 | mcpManifest: mcpManifest, 98 | } 99 | return t, nil 100 | } 101 | 102 | // validate interface 103 | var _ tools.Tool = Tool{} 104 | 105 | type Tool struct { 106 | Name string `yaml:"name"` 107 | Kind string `yaml:"kind"` 108 | AuthRequired []string `yaml:"authRequired"` 109 | Parameters tools.Parameters `yaml:"parameters"` 110 | 111 | Pool *sql.DB 112 | manifest tools.Manifest 113 | mcpManifest tools.McpManifest 114 | } 115 | 116 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 117 | paramsMap := params.AsMap() 118 | sql, ok := paramsMap["sql"].(string) 119 | if !ok { 120 | return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"]) 121 | } 122 | 123 | // Log the query executed for debugging. 124 | logger, err := util.LoggerFromContext(ctx) 125 | if err != nil { 126 | return nil, fmt.Errorf("error getting logger: %s", err) 127 | } 128 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql) 129 | 130 | results, err := t.Pool.QueryContext(ctx, sql) 131 | if err != nil { 132 | return nil, fmt.Errorf("unable to execute query: %w", err) 133 | } 134 | defer results.Close() 135 | 136 | cols, err := results.Columns() 137 | // If Columns() errors, it might be a DDL/DML without an OUTPUT clause. 138 | // We proceed, and results.Err() will catch actual query execution errors. 139 | // 'out' will remain nil if cols is empty or err is not nil here. 140 | 141 | var out []any 142 | if err == nil && len(cols) > 0 { 143 | // create an array of values for each column, which can be re-used to scan each row 144 | rawValues := make([]any, len(cols)) 145 | values := make([]any, len(cols)) 146 | for i := range rawValues { 147 | values[i] = &rawValues[i] 148 | } 149 | 150 | for results.Next() { 151 | scanErr := results.Scan(values...) 152 | if scanErr != nil { 153 | return nil, fmt.Errorf("unable to parse row: %w", scanErr) 154 | } 155 | vMap := make(map[string]any) 156 | for i, name := range cols { 157 | vMap[name] = rawValues[i] 158 | } 159 | out = append(out, vMap) 160 | } 161 | } 162 | 163 | // Check for errors from iterating over rows or from the query execution itself. 164 | // results.Close() is handled by defer. 165 | if err := results.Err(); err != nil { 166 | return nil, fmt.Errorf("errors encountered during query execution or row processing: %w", err) 167 | } 168 | 169 | return out, nil 170 | } 171 | 172 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 173 | return tools.ParseParams(t.Parameters, data, claims) 174 | } 175 | 176 | func (t Tool) Manifest() tools.Manifest { 177 | return t.manifest 178 | } 179 | 180 | func (t Tool) McpManifest() tools.McpManifest { 181 | return t.mcpManifest 182 | } 183 | 184 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 185 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 186 | } 187 | 188 | func (t Tool) RequiresClientAuthorization() bool { 189 | return false 190 | } 191 | ``` -------------------------------------------------------------------------------- /internal/tools/oceanbase/oceanbaseexecutesql/oceanbaseexecutesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oceanbaseexecutesql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/oceanbase" 25 | "github.com/googleapis/genai-toolbox/internal/tools" 26 | "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlcommon" 27 | ) 28 | 29 | const kind string = "oceanbase-execute-sql" 30 | 31 | func init() { 32 | if !tools.Register(kind, newConfig) { 33 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 34 | } 35 | } 36 | 37 | type compatibleSource interface { 38 | OceanBasePool() *sql.DB 39 | } 40 | 41 | // validate compatible sources are still compatible 42 | var _ compatibleSource = &oceanbase.Source{} 43 | 44 | var compatibleSources = [...]string{oceanbase.SourceKind} 45 | 46 | type Config struct { 47 | Name string `yaml:"name" validate:"required"` 48 | Kind string `yaml:"kind" validate:"required"` 49 | Source string `yaml:"source" validate:"required"` 50 | Description string `yaml:"description" validate:"required"` 51 | AuthRequired []string `yaml:"authRequired"` 52 | } 53 | 54 | // validate interface 55 | var _ tools.ToolConfig = Config{} 56 | 57 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 58 | actual := Config{Name: name} 59 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 60 | return nil, err 61 | } 62 | return actual, nil 63 | } 64 | 65 | func (cfg Config) ToolConfigKind() string { 66 | return kind 67 | } 68 | 69 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 70 | // verify source exists 71 | rawS, ok := srcs[cfg.Source] 72 | if !ok { 73 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 74 | } 75 | 76 | // verify the source is compatible 77 | s, ok := rawS.(compatibleSource) 78 | if !ok { 79 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 80 | } 81 | 82 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 83 | parameters := tools.Parameters{sqlParameter} 84 | 85 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 86 | 87 | // finish tool setup 88 | t := Tool{ 89 | Name: cfg.Name, 90 | Kind: kind, 91 | Parameters: parameters, 92 | AuthRequired: cfg.AuthRequired, 93 | Pool: s.OceanBasePool(), 94 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 95 | mcpManifest: mcpManifest, 96 | } 97 | return t, nil 98 | } 99 | 100 | // validate interface 101 | var _ tools.Tool = Tool{} 102 | 103 | type Tool struct { 104 | Name string `yaml:"name"` 105 | Kind string `yaml:"kind"` 106 | AuthRequired []string `yaml:"authRequired"` 107 | Parameters tools.Parameters `yaml:"parameters"` 108 | 109 | Pool *sql.DB 110 | manifest tools.Manifest 111 | mcpManifest tools.McpManifest 112 | } 113 | 114 | // Invoke executes the SQL statement provided in the parameters. 115 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 116 | sliceParams := params.AsSlice() 117 | sqlStr, ok := sliceParams[0].(string) 118 | if !ok { 119 | return nil, fmt.Errorf("unable to get cast %s", sliceParams[0]) 120 | } 121 | 122 | results, err := t.Pool.QueryContext(ctx, sqlStr) 123 | if err != nil { 124 | return nil, fmt.Errorf("unable to execute query: %w", err) 125 | } 126 | defer results.Close() 127 | 128 | cols, err := results.Columns() 129 | if err != nil { 130 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 131 | } 132 | 133 | // create an array of values for each column, which can be re-used to scan each row 134 | rawValues := make([]any, len(cols)) 135 | values := make([]any, len(cols)) 136 | for i := range rawValues { 137 | values[i] = &rawValues[i] 138 | } 139 | 140 | colTypes, err := results.ColumnTypes() 141 | if err != nil { 142 | return nil, fmt.Errorf("unable to get column types: %w", err) 143 | } 144 | 145 | var out []any 146 | for results.Next() { 147 | err := results.Scan(values...) 148 | if err != nil { 149 | return nil, fmt.Errorf("unable to parse row: %w", err) 150 | } 151 | vMap := make(map[string]any) 152 | for i, name := range cols { 153 | val := rawValues[i] 154 | if val == nil { 155 | vMap[name] = nil 156 | continue 157 | } 158 | 159 | // oceanbase uses mysql driver 160 | vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val) 161 | if err != nil { 162 | return nil, fmt.Errorf("errors encountered when converting values: %w", err) 163 | } 164 | } 165 | out = append(out, vMap) 166 | } 167 | 168 | if err := results.Err(); err != nil { 169 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 170 | } 171 | 172 | return out, nil 173 | } 174 | 175 | // ParseParams parses the input parameters for the tool. 176 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 177 | return tools.ParseParams(t.Parameters, data, claims) 178 | } 179 | 180 | // Manifest returns the tool manifest. 181 | func (t Tool) Manifest() tools.Manifest { 182 | return t.manifest 183 | } 184 | 185 | // McpManifest returns the MCP manifest for the tool. 186 | func (t Tool) McpManifest() tools.McpManifest { 187 | return t.mcpManifest 188 | } 189 | 190 | // Authorized checks if the tool is authorized for the given auth services. 191 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 192 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 193 | } 194 | 195 | func (t Tool) RequiresClientAuthorization() bool { 196 | return false 197 | } 198 | ``` -------------------------------------------------------------------------------- /tests/cloudsql/cloud_sql_list_databases_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudsql 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httptest" 25 | "net/url" 26 | "reflect" 27 | "regexp" 28 | "strings" 29 | "testing" 30 | "time" 31 | 32 | "github.com/googleapis/genai-toolbox/internal/testutils" 33 | "github.com/googleapis/genai-toolbox/tests" 34 | ) 35 | 36 | var ( 37 | listDatabasesToolKind = "cloud-sql-list-databases" 38 | ) 39 | 40 | type listDatabasesTransport struct { 41 | transport http.RoundTripper 42 | url *url.URL 43 | } 44 | 45 | func (t *listDatabasesTransport) RoundTrip(req *http.Request) (*http.Response, error) { 46 | if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { 47 | req.URL.Scheme = t.url.Scheme 48 | req.URL.Host = t.url.Host 49 | } 50 | return t.transport.RoundTrip(req) 51 | } 52 | 53 | type masterListDatabasesHandler struct { 54 | t *testing.T 55 | } 56 | 57 | func (h *masterListDatabasesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | if !strings.Contains(r.UserAgent(), "genai-toolbox/") { 59 | h.t.Errorf("User-Agent header not found") 60 | } 61 | 62 | response := map[string]any{ 63 | "items": []map[string]any{ 64 | { 65 | "name": "db1", 66 | "charset": "utf8", 67 | "collation": "utf8_general_ci", 68 | }, 69 | { 70 | "name": "db2", 71 | "charset": "utf8mb4", 72 | "collation": "utf8mb4_unicode_ci", 73 | }, 74 | }, 75 | } 76 | statusCode := http.StatusOK 77 | 78 | w.Header().Set("Content-Type", "application/json") 79 | w.WriteHeader(statusCode) 80 | if err := json.NewEncoder(w).Encode(response); err != nil { 81 | http.Error(w, err.Error(), http.StatusInternalServerError) 82 | } 83 | } 84 | 85 | func TestListDatabasesToolEndpoints(t *testing.T) { 86 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 87 | defer cancel() 88 | 89 | handler := &masterListDatabasesHandler{t: t} 90 | server := httptest.NewServer(handler) 91 | defer server.Close() 92 | 93 | serverURL, err := url.Parse(server.URL) 94 | if err != nil { 95 | t.Fatalf("failed to parse server URL: %v", err) 96 | } 97 | 98 | originalTransport := http.DefaultClient.Transport 99 | if originalTransport == nil { 100 | originalTransport = http.DefaultTransport 101 | } 102 | http.DefaultClient.Transport = &listDatabasesTransport{ 103 | transport: originalTransport, 104 | url: serverURL, 105 | } 106 | t.Cleanup(func() { 107 | http.DefaultClient.Transport = originalTransport 108 | }) 109 | 110 | var args []string 111 | toolsFile := getListDatabasesToolsConfig() 112 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 113 | if err != nil { 114 | t.Fatalf("command initialization returned an error: %s", err) 115 | } 116 | defer cleanup() 117 | 118 | waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) 119 | defer cancel() 120 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 121 | if err != nil { 122 | t.Logf("toolbox command logs: \n%s", out) 123 | t.Fatalf("toolbox didn't start successfully: %s", err) 124 | } 125 | 126 | tcs := []struct { 127 | name string 128 | toolName string 129 | body string 130 | want string 131 | expectError bool 132 | errorStatus int 133 | }{ 134 | { 135 | name: "successful databases listing", 136 | toolName: "list-databases", 137 | body: `{"project": "p1", "instance": "i1"}`, 138 | want: `[{"name":"db1","charset":"utf8","collation":"utf8_general_ci"},{"name":"db2","charset":"utf8mb4","collation":"utf8mb4_unicode_ci"}]`, 139 | }, 140 | { 141 | name: "missing instance", 142 | toolName: "list-databases", 143 | body: `{"project": "p1"}`, 144 | expectError: true, 145 | errorStatus: http.StatusBadRequest, 146 | }, 147 | } 148 | 149 | for _, tc := range tcs { 150 | tc := tc 151 | t.Run(tc.name, func(t *testing.T) { 152 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 153 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 154 | if err != nil { 155 | t.Fatalf("unable to create request: %s", err) 156 | } 157 | req.Header.Add("Content-type", "application/json") 158 | resp, err := http.DefaultClient.Do(req) 159 | if err != nil { 160 | t.Fatalf("unable to send request: %s", err) 161 | } 162 | defer resp.Body.Close() 163 | 164 | if tc.expectError { 165 | if resp.StatusCode != tc.errorStatus { 166 | bodyBytes, _ := io.ReadAll(resp.Body) 167 | t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) 168 | } 169 | return 170 | } 171 | 172 | if resp.StatusCode != http.StatusOK { 173 | bodyBytes, _ := io.ReadAll(resp.Body) 174 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 175 | } 176 | 177 | var result struct { 178 | Result string `json:"result"` 179 | } 180 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 181 | t.Fatalf("failed to decode response: %v", err) 182 | } 183 | 184 | var got, want []map[string]any 185 | if err := json.Unmarshal([]byte(result.Result), &got); err != nil { 186 | t.Fatalf("failed to unmarshal result: %v", err) 187 | } 188 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 189 | t.Fatalf("failed to unmarshal want: %v", err) 190 | } 191 | 192 | if !reflect.DeepEqual(got, want) { 193 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func getListDatabasesToolsConfig() map[string]any { 200 | return map[string]any{ 201 | "sources": map[string]any{ 202 | "my-cloud-sql-source": map[string]any{ 203 | "kind": "cloud-sql-admin", 204 | }, 205 | }, 206 | "tools": map[string]any{ 207 | "list-databases": map[string]any{ 208 | "kind": listDatabasesToolKind, 209 | "source": "my-cloud-sql-source", 210 | }, 211 | }, 212 | } 213 | } 214 | ``` -------------------------------------------------------------------------------- /internal/sources/cloudsqlpg/cloud_sql_pg_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudsqlpg_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg" 24 | "github.com/googleapis/genai-toolbox/internal/testutils" 25 | ) 26 | 27 | func TestParseFromYamlCloudSQLPg(t *testing.T) { 28 | tcs := []struct { 29 | desc string 30 | in string 31 | want server.SourceConfigs 32 | }{ 33 | { 34 | desc: "basic example", 35 | in: ` 36 | sources: 37 | my-pg-instance: 38 | kind: cloud-sql-postgres 39 | project: my-project 40 | region: my-region 41 | instance: my-instance 42 | database: my_db 43 | user: my_user 44 | password: my_pass 45 | `, 46 | want: server.SourceConfigs{ 47 | "my-pg-instance": cloudsqlpg.Config{ 48 | Name: "my-pg-instance", 49 | Kind: cloudsqlpg.SourceKind, 50 | Project: "my-project", 51 | Region: "my-region", 52 | Instance: "my-instance", 53 | IPType: "public", 54 | Database: "my_db", 55 | User: "my_user", 56 | Password: "my_pass", 57 | }, 58 | }, 59 | }, 60 | { 61 | desc: "public ipType", 62 | in: ` 63 | sources: 64 | my-pg-instance: 65 | kind: cloud-sql-postgres 66 | project: my-project 67 | region: my-region 68 | instance: my-instance 69 | ipType: Public 70 | database: my_db 71 | user: my_user 72 | password: my_pass 73 | `, 74 | want: server.SourceConfigs{ 75 | "my-pg-instance": cloudsqlpg.Config{ 76 | Name: "my-pg-instance", 77 | Kind: cloudsqlpg.SourceKind, 78 | Project: "my-project", 79 | Region: "my-region", 80 | Instance: "my-instance", 81 | IPType: "public", 82 | Database: "my_db", 83 | User: "my_user", 84 | Password: "my_pass", 85 | }, 86 | }, 87 | }, 88 | { 89 | desc: "private ipType", 90 | in: ` 91 | sources: 92 | my-pg-instance: 93 | kind: cloud-sql-postgres 94 | project: my-project 95 | region: my-region 96 | instance: my-instance 97 | ipType: private 98 | database: my_db 99 | user: my_user 100 | password: my_pass 101 | `, 102 | want: server.SourceConfigs{ 103 | "my-pg-instance": cloudsqlpg.Config{ 104 | Name: "my-pg-instance", 105 | Kind: cloudsqlpg.SourceKind, 106 | Project: "my-project", 107 | Region: "my-region", 108 | Instance: "my-instance", 109 | IPType: "private", 110 | Database: "my_db", 111 | User: "my_user", 112 | Password: "my_pass", 113 | }, 114 | }, 115 | }, 116 | { 117 | desc: "psc ipType", 118 | in: ` 119 | sources: 120 | my-pg-instance: 121 | kind: cloud-sql-postgres 122 | project: my-project 123 | region: my-region 124 | instance: my-instance 125 | ipType: psc 126 | database: my_db 127 | user: my_user 128 | password: my_pass 129 | `, 130 | want: server.SourceConfigs{ 131 | "my-pg-instance": cloudsqlpg.Config{ 132 | Name: "my-pg-instance", 133 | Kind: cloudsqlpg.SourceKind, 134 | Project: "my-project", 135 | Region: "my-region", 136 | Instance: "my-instance", 137 | IPType: "psc", 138 | Database: "my_db", 139 | User: "my_user", 140 | Password: "my_pass", 141 | }, 142 | }, 143 | }, 144 | } 145 | for _, tc := range tcs { 146 | t.Run(tc.desc, func(t *testing.T) { 147 | got := struct { 148 | Sources server.SourceConfigs `yaml:"sources"` 149 | }{} 150 | // Parse contents 151 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 152 | if err != nil { 153 | t.Fatalf("unable to unmarshal: %s", err) 154 | } 155 | if !cmp.Equal(tc.want, got.Sources) { 156 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 157 | } 158 | }) 159 | } 160 | 161 | } 162 | 163 | func TestFailParseFromYaml(t *testing.T) { 164 | tcs := []struct { 165 | desc string 166 | in string 167 | err string 168 | }{ 169 | { 170 | desc: "invalid ipType", 171 | in: ` 172 | sources: 173 | my-pg-instance: 174 | kind: cloud-sql-postgres 175 | project: my-project 176 | region: my-region 177 | instance: my-instance 178 | ipType: fail 179 | database: my_db 180 | user: my_user 181 | password: my_pass 182 | `, 183 | err: "unable to parse source \"my-pg-instance\" as \"cloud-sql-postgres\": ipType invalid: must be one of \"public\", \"private\", or \"psc\"", 184 | }, 185 | { 186 | desc: "extra field", 187 | in: ` 188 | sources: 189 | my-pg-instance: 190 | kind: cloud-sql-postgres 191 | project: my-project 192 | region: my-region 193 | instance: my-instance 194 | database: my_db 195 | user: my_user 196 | password: my_pass 197 | foo: bar 198 | `, 199 | err: "unable to parse source \"my-pg-instance\" as \"cloud-sql-postgres\": [2:1] unknown field \"foo\"\n 1 | database: my_db\n> 2 | foo: bar\n ^\n 3 | instance: my-instance\n 4 | kind: cloud-sql-postgres\n 5 | password: my_pass\n 6 | ", 200 | }, 201 | { 202 | desc: "missing required field", 203 | in: ` 204 | sources: 205 | my-pg-instance: 206 | kind: cloud-sql-postgres 207 | region: my-region 208 | instance: my-instance 209 | database: my_db 210 | user: my_user 211 | password: my_pass 212 | `, 213 | err: "unable to parse source \"my-pg-instance\" as \"cloud-sql-postgres\": Key: 'Config.Project' Error:Field validation for 'Project' failed on the 'required' tag", 214 | }, 215 | } 216 | for _, tc := range tcs { 217 | t.Run(tc.desc, func(t *testing.T) { 218 | got := struct { 219 | Sources server.SourceConfigs `yaml:"sources"` 220 | }{} 221 | // Parse contents 222 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 223 | if err == nil { 224 | t.Fatalf("expect parsing to fail") 225 | } 226 | errStr := err.Error() 227 | if errStr != tc.err { 228 | t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) 229 | } 230 | }) 231 | } 232 | } 233 | ``` -------------------------------------------------------------------------------- /internal/tools/yugabytedbsql/yugabytedbsql_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yugabytedbsql_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/yugabytedbsql" 26 | ) 27 | 28 | func TestParseFromYamlYugabyteDBSQL(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic valid config", 40 | in: ` 41 | tools: 42 | hotel_search: 43 | kind: yugabytedb-sql 44 | source: yb-source 45 | description: search hotels by city 46 | statement: | 47 | SELECT * FROM hotels WHERE city = $1; 48 | authRequired: 49 | - auth-service-a 50 | - auth-service-b 51 | parameters: 52 | - name: city 53 | type: string 54 | description: city name 55 | authServices: 56 | - name: auth-service-a 57 | field: user_id 58 | - name: auth-service-b 59 | field: user_id 60 | `, 61 | want: server.ToolConfigs{ 62 | "hotel_search": yugabytedbsql.Config{ 63 | Name: "hotel_search", 64 | Kind: "yugabytedb-sql", 65 | Source: "yb-source", 66 | Description: "search hotels by city", 67 | Statement: "SELECT * FROM hotels WHERE city = $1;\n", 68 | AuthRequired: []string{"auth-service-a", "auth-service-b"}, 69 | Parameters: []tools.Parameter{ 70 | tools.NewStringParameterWithAuth("city", "city name", 71 | []tools.ParamAuthService{ 72 | {Name: "auth-service-a", Field: "user_id"}, 73 | {Name: "auth-service-b", Field: "user_id"}, 74 | }, 75 | ), 76 | }, 77 | }, 78 | }, 79 | }, 80 | } 81 | 82 | for _, tc := range tcs { 83 | t.Run(tc.desc, func(t *testing.T) { 84 | got := struct { 85 | Tools server.ToolConfigs `yaml:"tools"` 86 | }{} 87 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 88 | if err != nil { 89 | t.Fatalf("unable to unmarshal: %s", err) 90 | } 91 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 92 | t.Fatalf("incorrect parse: diff %v", diff) 93 | } 94 | }) 95 | } 96 | } 97 | 98 | func TestFailParseFromYamlYugabyteDBSQL(t *testing.T) { 99 | ctx, err := testutils.ContextWithNewLogger() 100 | if err != nil { 101 | t.Fatalf("unexpected error: %s", err) 102 | } 103 | cases := []struct { 104 | desc string 105 | in string 106 | }{ 107 | { 108 | desc: "missing required field (statement)", 109 | in: ` 110 | tools: 111 | tool1: 112 | kind: yugabytedb-sql 113 | source: yb-source 114 | description: incomplete config 115 | `, 116 | }, 117 | { 118 | desc: "unknown field (foo)", 119 | in: ` 120 | tools: 121 | tool2: 122 | kind: yugabytedb-sql 123 | source: yb-source 124 | description: test 125 | statement: SELECT 1; 126 | foo: bar 127 | `, 128 | }, 129 | } 130 | for _, tc := range cases { 131 | t.Run(tc.desc, func(t *testing.T) { 132 | cfg := struct { 133 | Tools server.ToolConfigs `yaml:"tools"` 134 | }{} 135 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &cfg) 136 | if err == nil { 137 | t.Fatalf("expected error but got none") 138 | } 139 | }) 140 | } 141 | } 142 | 143 | func TestParseFromYamlWithTemplateParamsYugabyteDB(t *testing.T) { 144 | ctx, err := testutils.ContextWithNewLogger() 145 | if err != nil { 146 | t.Fatalf("unexpected error: %s", err) 147 | } 148 | tcs := []struct { 149 | desc string 150 | in string 151 | want server.ToolConfigs 152 | }{ 153 | { 154 | desc: "basic example", 155 | in: ` 156 | tools: 157 | example_tool: 158 | kind: yugabytedb-sql 159 | source: my-yb-instance 160 | description: some description 161 | statement: | 162 | SELECT * FROM SQL_STATEMENT; 163 | parameters: 164 | - name: name 165 | type: string 166 | description: some description 167 | templateParameters: 168 | - name: tableName 169 | type: string 170 | description: The table to select hotels from. 171 | - name: fieldArray 172 | type: array 173 | description: The columns to return for the query. 174 | items: 175 | name: column 176 | type: string 177 | description: A column name that will be returned from the query. 178 | `, 179 | want: server.ToolConfigs{ 180 | "example_tool": yugabytedbsql.Config{ 181 | Name: "example_tool", 182 | Kind: "yugabytedb-sql", 183 | Source: "my-yb-instance", 184 | Description: "some description", 185 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 186 | AuthRequired: []string{}, 187 | Parameters: []tools.Parameter{ 188 | tools.NewStringParameter("name", "some description"), 189 | }, 190 | TemplateParameters: []tools.Parameter{ 191 | tools.NewStringParameter("tableName", "The table to select hotels from."), 192 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 193 | }, 194 | }, 195 | }, 196 | }, 197 | } 198 | for _, tc := range tcs { 199 | t.Run(tc.desc, func(t *testing.T) { 200 | got := struct { 201 | Tools server.ToolConfigs `yaml:"tools"` 202 | }{} 203 | // Parse contents 204 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 205 | if err != nil { 206 | t.Fatalf("unable to unmarshal: %s", err) 207 | } 208 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 209 | t.Fatalf("incorrect parse: diff %v", diff) 210 | } 211 | }) 212 | } 213 | 214 | } 215 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/firebird/firebird-sql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "firebird-sql" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "firebird-sql" tool executes a pre-defined SQL statement against a Firebird 7 | database. 8 | aliases: 9 | - /resources/tools/firebird-sql 10 | --- 11 | 12 | ## About 13 | 14 | A `firebird-sql` tool executes a pre-defined SQL statement against a Firebird 15 | database. It's compatible with the following source: 16 | 17 | - [firebird](../sources/firebird.md) 18 | 19 | The specified SQL statement is executed as a [prepared statement][fb-prepare], 20 | and supports both positional parameters (`?`) and named parameters (`:param_name`). 21 | Parameters will be inserted according to their position or name. If template 22 | parameters are included, they will be resolved before the execution of the 23 | prepared statement. 24 | 25 | [fb-prepare]: https://firebirdsql.org/refdocs/langrefupd25-psql-execstat.html 26 | 27 | ## Example 28 | 29 | > **Note:** This tool uses parameterized queries to prevent SQL injections. 30 | > Query parameters can be used as substitutes for arbitrary expressions. 31 | > Parameters cannot be used as substitutes for identifiers, column names, table 32 | > names, or other parts of the query. 33 | 34 | ```yaml 35 | tools: 36 | search_flights_by_number: 37 | kind: firebird-sql 38 | source: my_firebird_db 39 | statement: | 40 | SELECT * FROM flights 41 | WHERE airline = ? 42 | AND flight_number = ? 43 | LIMIT 10 44 | description: | 45 | Use this tool to get information for a specific flight. 46 | Takes an airline code and flight number and returns info on the flight. 47 | Do NOT use this tool with a flight id. Do NOT guess an airline code or flight number. 48 | A airline code is a code for an airline service consisting of two-character 49 | airline designator and followed by flight number, which is 1 to 4 digit number. 50 | For example, if given CY 0123, the airline is "CY", and flight_number is "123". 51 | Another example for this is DL 1234, the airline is "DL", and flight_number is "1234". 52 | If the tool returns more than one option choose the date closes to today. 53 | Example: 54 | {{ 55 | "airline": "CY", 56 | "flight_number": "888", 57 | }} 58 | Example: 59 | {{ 60 | "airline": "DL", 61 | "flight_number": "1234", 62 | }} 63 | parameters: 64 | - name: airline 65 | type: string 66 | description: Airline unique 2 letter identifier 67 | - name: flight_number 68 | type: string 69 | description: 1 to 4 digit number 70 | ``` 71 | 72 | ### Example with Named Parameters 73 | 74 | ```yaml 75 | tools: 76 | search_flights_by_airline: 77 | kind: firebird-sql 78 | source: my_firebird_db 79 | statement: | 80 | SELECT * FROM flights 81 | WHERE airline = :airline 82 | AND departure_date >= :start_date 83 | AND departure_date <= :end_date 84 | ORDER BY departure_date 85 | description: | 86 | Search for flights by airline within a date range using named parameters. 87 | parameters: 88 | - name: airline 89 | type: string 90 | description: Airline unique 2 letter identifier 91 | - name: start_date 92 | type: string 93 | description: Start date in YYYY-MM-DD format 94 | - name: end_date 95 | type: string 96 | description: End date in YYYY-MM-DD format 97 | ``` 98 | 99 | ### Example with Template Parameters 100 | 101 | > **Note:** This tool allows direct modifications to the SQL statement, 102 | > including identifiers, column names, and table names. **This makes it more 103 | > vulnerable to SQL injections**. Using basic parameters only (see above) is 104 | > recommended for performance and safety reasons. For more details, please check 105 | > [templateParameters](_index#template-parameters). 106 | 107 | ```yaml 108 | tools: 109 | list_table: 110 | kind: firebird-sql 111 | source: my_firebird_db 112 | statement: | 113 | SELECT * FROM {{.tableName}} 114 | description: | 115 | Use this tool to list all information from a specific table. 116 | Example: 117 | {{ 118 | "tableName": "flights", 119 | }} 120 | templateParameters: 121 | - name: tableName 122 | type: string 123 | description: Table to select from 124 | ``` 125 | 126 | ## Reference 127 | 128 | | **field** | **type** | **required** | **description** | 129 | |--------------------|:------------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------| 130 | | kind | string | true | Must be "firebird-sql". | 131 | | source | string | true | Name of the source the SQL should execute on. | 132 | | description | string | true | Description of the tool that is passed to the LLM. | 133 | | statement | string | true | SQL statement to execute on. | 134 | | parameters | [parameters](_index#specifying-parameters) | false | List of [parameters](_index#specifying-parameters) that will be inserted into the SQL statement. | 135 | | templateParameters | [templateParameters](_index#template-parameters) | false | List of [templateParameters](_index#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | ``` -------------------------------------------------------------------------------- /internal/tools/mysql/mysqlexecutesql/mysqlexecutesql.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mysqlexecutesql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | yaml "github.com/goccy/go-yaml" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmysql" 25 | "github.com/googleapis/genai-toolbox/internal/sources/mysql" 26 | "github.com/googleapis/genai-toolbox/internal/tools" 27 | "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlcommon" 28 | "github.com/googleapis/genai-toolbox/internal/util" 29 | ) 30 | 31 | const kind string = "mysql-execute-sql" 32 | 33 | func init() { 34 | if !tools.Register(kind, newConfig) { 35 | panic(fmt.Sprintf("tool kind %q already registered", kind)) 36 | } 37 | } 38 | 39 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { 40 | actual := Config{Name: name} 41 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 42 | return nil, err 43 | } 44 | return actual, nil 45 | } 46 | 47 | type compatibleSource interface { 48 | MySQLPool() *sql.DB 49 | } 50 | 51 | // validate compatible sources are still compatible 52 | var _ compatibleSource = &cloudsqlmysql.Source{} 53 | var _ compatibleSource = &mysql.Source{} 54 | 55 | var compatibleSources = [...]string{cloudsqlmysql.SourceKind, mysql.SourceKind} 56 | 57 | type Config struct { 58 | Name string `yaml:"name" validate:"required"` 59 | Kind string `yaml:"kind" validate:"required"` 60 | Source string `yaml:"source" validate:"required"` 61 | Description string `yaml:"description" validate:"required"` 62 | AuthRequired []string `yaml:"authRequired"` 63 | } 64 | 65 | // validate interface 66 | var _ tools.ToolConfig = Config{} 67 | 68 | func (cfg Config) ToolConfigKind() string { 69 | return kind 70 | } 71 | 72 | func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { 73 | // verify source exists 74 | rawS, ok := srcs[cfg.Source] 75 | if !ok { 76 | return nil, fmt.Errorf("no source named %q configured", cfg.Source) 77 | } 78 | 79 | // verify the source is compatible 80 | s, ok := rawS.(compatibleSource) 81 | if !ok { 82 | return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources) 83 | } 84 | 85 | sqlParameter := tools.NewStringParameter("sql", "The sql to execute.") 86 | parameters := tools.Parameters{sqlParameter} 87 | 88 | mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters) 89 | 90 | // finish tool setup 91 | t := Tool{ 92 | Name: cfg.Name, 93 | Kind: kind, 94 | Parameters: parameters, 95 | AuthRequired: cfg.AuthRequired, 96 | Pool: s.MySQLPool(), 97 | manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired}, 98 | mcpManifest: mcpManifest, 99 | } 100 | return t, nil 101 | } 102 | 103 | // validate interface 104 | var _ tools.Tool = Tool{} 105 | 106 | type Tool struct { 107 | Name string `yaml:"name"` 108 | Kind string `yaml:"kind"` 109 | AuthRequired []string `yaml:"authRequired"` 110 | Parameters tools.Parameters `yaml:"parameters"` 111 | 112 | Pool *sql.DB 113 | manifest tools.Manifest 114 | mcpManifest tools.McpManifest 115 | } 116 | 117 | func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) { 118 | paramsMap := params.AsMap() 119 | sql, ok := paramsMap["sql"].(string) 120 | if !ok { 121 | return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"]) 122 | } 123 | 124 | // Log the query executed for debugging. 125 | logger, err := util.LoggerFromContext(ctx) 126 | if err != nil { 127 | return nil, fmt.Errorf("error getting logger: %s", err) 128 | } 129 | logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql) 130 | 131 | results, err := t.Pool.QueryContext(ctx, sql) 132 | if err != nil { 133 | return nil, fmt.Errorf("unable to execute query: %w", err) 134 | } 135 | defer results.Close() 136 | 137 | cols, err := results.Columns() 138 | if err != nil { 139 | return nil, fmt.Errorf("unable to retrieve rows column name: %w", err) 140 | } 141 | 142 | // create an array of values for each column, which can be re-used to scan each row 143 | rawValues := make([]any, len(cols)) 144 | values := make([]any, len(cols)) 145 | for i := range rawValues { 146 | values[i] = &rawValues[i] 147 | } 148 | 149 | colTypes, err := results.ColumnTypes() 150 | if err != nil { 151 | return nil, fmt.Errorf("unable to get column types: %w", err) 152 | } 153 | 154 | var out []any 155 | for results.Next() { 156 | err := results.Scan(values...) 157 | if err != nil { 158 | return nil, fmt.Errorf("unable to parse row: %w", err) 159 | } 160 | vMap := make(map[string]any) 161 | for i, name := range cols { 162 | val := rawValues[i] 163 | if val == nil { 164 | vMap[name] = nil 165 | continue 166 | } 167 | 168 | vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val) 169 | if err != nil { 170 | return nil, fmt.Errorf("errors encountered when converting values: %w", err) 171 | } 172 | } 173 | out = append(out, vMap) 174 | } 175 | 176 | if err := results.Err(); err != nil { 177 | return nil, fmt.Errorf("errors encountered during row iteration: %w", err) 178 | } 179 | 180 | return out, nil 181 | } 182 | 183 | func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) { 184 | return tools.ParseParams(t.Parameters, data, claims) 185 | } 186 | 187 | func (t Tool) Manifest() tools.Manifest { 188 | return t.manifest 189 | } 190 | 191 | func (t Tool) McpManifest() tools.McpManifest { 192 | return t.mcpManifest 193 | } 194 | 195 | func (t Tool) Authorized(verifiedAuthServices []string) bool { 196 | return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) 197 | } 198 | 199 | func (t Tool) RequiresClientAuthorization() bool { 200 | return false 201 | } 202 | ```