This is page 24 of 48. 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/common_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 tools_test 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | "text/template" 21 | 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/googleapis/genai-toolbox/internal/tools" 24 | ) 25 | 26 | func TestPopulateTemplate(t *testing.T) { 27 | tcs := []struct { 28 | name string 29 | templateName string 30 | templateString string 31 | data map[string]any 32 | want string 33 | wantErr bool 34 | }{ 35 | { 36 | name: "simple string substitution", 37 | templateName: "test", 38 | templateString: "Hello {{.name}}!", 39 | data: map[string]any{"name": "World"}, 40 | want: "Hello World!", 41 | wantErr: false, 42 | }, 43 | { 44 | name: "multiple substitutions", 45 | templateName: "test", 46 | templateString: "{{.greeting}} {{.name}}, you are {{.age}} years old", 47 | data: map[string]any{"greeting": "Hello", "name": "Alice", "age": 30}, 48 | want: "Hello Alice, you are 30 years old", 49 | wantErr: false, 50 | }, 51 | { 52 | name: "empty template", 53 | templateName: "test", 54 | templateString: "", 55 | data: map[string]any{}, 56 | want: "", 57 | wantErr: false, 58 | }, 59 | { 60 | name: "no substitutions", 61 | templateName: "test", 62 | templateString: "Plain text without templates", 63 | data: map[string]any{}, 64 | want: "Plain text without templates", 65 | wantErr: false, 66 | }, 67 | { 68 | name: "invalid template syntax", 69 | templateName: "test", 70 | templateString: "{{.name", 71 | data: map[string]any{"name": "World"}, 72 | want: "", 73 | wantErr: true, 74 | }, 75 | { 76 | name: "missing field", 77 | templateName: "test", 78 | templateString: "{{.missing}}", 79 | data: map[string]any{"name": "World"}, 80 | want: "<no value>", 81 | wantErr: false, 82 | }, 83 | { 84 | name: "invalid function call", 85 | templateName: "test", 86 | templateString: "{{.name.invalid}}", 87 | data: map[string]any{"name": "World"}, 88 | want: "", 89 | wantErr: true, 90 | }, 91 | } 92 | 93 | for _, tc := range tcs { 94 | t.Run(tc.name, func(t *testing.T) { 95 | got, err := tools.PopulateTemplate(tc.templateName, tc.templateString, tc.data) 96 | if tc.wantErr { 97 | if err == nil { 98 | t.Fatalf("expected error, got nil") 99 | } 100 | return 101 | } 102 | if err != nil { 103 | t.Fatalf("unexpected error: %s", err) 104 | } 105 | if diff := cmp.Diff(tc.want, got); diff != "" { 106 | t.Fatalf("incorrect result (-want +got):\n%s", diff) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestPopulateTemplateWithFunc(t *testing.T) { 113 | // Custom function for testing 114 | customFuncs := template.FuncMap{ 115 | "upper": strings.ToUpper, 116 | "add": func(a, b int) int { 117 | return a + b 118 | }, 119 | } 120 | 121 | tcs := []struct { 122 | name string 123 | templateName string 124 | templateString string 125 | data map[string]any 126 | funcMap template.FuncMap 127 | want string 128 | wantErr bool 129 | }{ 130 | { 131 | name: "with custom upper function", 132 | templateName: "test", 133 | templateString: "{{upper .text}}", 134 | data: map[string]any{"text": "hello"}, 135 | funcMap: customFuncs, 136 | want: "HELLO", 137 | wantErr: false, 138 | }, 139 | { 140 | name: "with custom add function", 141 | templateName: "test", 142 | templateString: "Result: {{add .x .y}}", 143 | data: map[string]any{"x": 5, "y": 3}, 144 | funcMap: customFuncs, 145 | want: "Result: 8", 146 | wantErr: false, 147 | }, 148 | { 149 | name: "nil funcMap", 150 | templateName: "test", 151 | templateString: "Hello {{.name}}", 152 | data: map[string]any{"name": "World"}, 153 | funcMap: nil, 154 | want: "Hello World", 155 | wantErr: false, 156 | }, 157 | { 158 | name: "combine custom function with regular substitution", 159 | templateName: "test", 160 | templateString: "{{upper .greeting}} {{.name}}!", 161 | data: map[string]any{"greeting": "hello", "name": "Alice"}, 162 | funcMap: customFuncs, 163 | want: "HELLO Alice!", 164 | wantErr: false, 165 | }, 166 | { 167 | name: "undefined function", 168 | templateName: "test", 169 | templateString: "{{undefined .text}}", 170 | data: map[string]any{"text": "hello"}, 171 | funcMap: nil, 172 | want: "", 173 | wantErr: true, 174 | }, 175 | { 176 | name: "wrong number of arguments", 177 | templateName: "test", 178 | templateString: "{{upper}}", 179 | data: map[string]any{}, 180 | funcMap: template.FuncMap{"upper": strings.ToUpper}, 181 | want: "", 182 | wantErr: true, 183 | }, 184 | } 185 | 186 | for _, tc := range tcs { 187 | t.Run(tc.name, func(t *testing.T) { 188 | got, err := tools.PopulateTemplateWithFunc(tc.templateName, tc.templateString, tc.data, tc.funcMap) 189 | if tc.wantErr { 190 | if err == nil { 191 | t.Fatalf("expected error, got nil") 192 | } 193 | return 194 | } 195 | if err != nil { 196 | t.Fatalf("unexpected error: %s", err) 197 | } 198 | if diff := cmp.Diff(tc.want, got); diff != "" { 199 | t.Fatalf("incorrect result (-want +got):\n%s", diff) 200 | } 201 | }) 202 | } 203 | } 204 | 205 | func TestPopulateTemplateWithJSON(t *testing.T) { 206 | tcs := []struct { 207 | name string 208 | templateName string 209 | templateString string 210 | data map[string]any 211 | want string 212 | wantErr bool 213 | }{ 214 | { 215 | name: "json string", 216 | templateName: "test", 217 | templateString: "Data: {{json .value}}", 218 | data: map[string]any{"value": "hello"}, 219 | want: `Data: "hello"`, 220 | wantErr: false, 221 | }, 222 | { 223 | name: "json number", 224 | templateName: "test", 225 | templateString: "Number: {{json .num}}", 226 | data: map[string]any{"num": 42}, 227 | want: "Number: 42", 228 | wantErr: false, 229 | }, 230 | { 231 | name: "json boolean", 232 | templateName: "test", 233 | templateString: "Bool: {{json .flag}}", 234 | data: map[string]any{"flag": true}, 235 | want: "Bool: true", 236 | wantErr: false, 237 | }, 238 | { 239 | name: "json array", 240 | templateName: "test", 241 | templateString: "Array: {{json .items}}", 242 | data: map[string]any{"items": []any{"a", "b", "c"}}, 243 | want: `Array: ["a","b","c"]`, 244 | wantErr: false, 245 | }, 246 | { 247 | name: "json object", 248 | templateName: "test", 249 | templateString: "Object: {{json .obj}}", 250 | data: map[string]any{"obj": map[string]any{"name": "Alice", "age": 30}}, 251 | want: `Object: {"age":30,"name":"Alice"}`, 252 | wantErr: false, 253 | }, 254 | { 255 | name: "json null", 256 | templateName: "test", 257 | templateString: "Null: {{json .nullValue}}", 258 | data: map[string]any{"nullValue": nil}, 259 | want: "Null: null", 260 | wantErr: false, 261 | }, 262 | { 263 | name: "combine json with regular substitution", 264 | templateName: "test", 265 | templateString: "User {{.name}} has data: {{json .data}}", 266 | data: map[string]any{"name": "Bob", "data": map[string]any{"id": 123}}, 267 | want: `User Bob has data: {"id":123}`, 268 | wantErr: false, 269 | }, 270 | { 271 | name: "missing field for json", 272 | templateName: "test", 273 | templateString: "{{json .missing}}", 274 | data: map[string]any{}, 275 | want: "null", 276 | wantErr: false, 277 | }, 278 | } 279 | 280 | for _, tc := range tcs { 281 | t.Run(tc.name, func(t *testing.T) { 282 | got, err := tools.PopulateTemplateWithJSON(tc.templateName, tc.templateString, tc.data) 283 | if tc.wantErr { 284 | if err == nil { 285 | t.Fatalf("expected error, got nil") 286 | } 287 | return 288 | } 289 | if err != nil { 290 | t.Fatalf("unexpected error: %s", err) 291 | } 292 | if diff := cmp.Diff(tc.want, got); diff != "" { 293 | t.Fatalf("incorrect result (-want +got):\n%s", diff) 294 | } 295 | }) 296 | } 297 | } 298 | ``` -------------------------------------------------------------------------------- /tests/cloudsqlmysql/cloud_sql_mysql_create_instance_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudsqlmysql_test 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/google/go-cmp/cmp" 33 | "github.com/googleapis/genai-toolbox/internal/testutils" 34 | "github.com/googleapis/genai-toolbox/tests" 35 | "google.golang.org/api/sqladmin/v1" 36 | ) 37 | 38 | var ( 39 | createInstanceToolKind = "cloud-sql-mysql-create-instance" 40 | ) 41 | 42 | type createInstanceTransport struct { 43 | transport http.RoundTripper 44 | url *url.URL 45 | } 46 | 47 | func (t *createInstanceTransport) RoundTrip(req *http.Request) (*http.Response, error) { 48 | if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { 49 | req.URL.Scheme = t.url.Scheme 50 | req.URL.Host = t.url.Host 51 | } 52 | return t.transport.RoundTrip(req) 53 | } 54 | 55 | type masterHandler struct { 56 | t *testing.T 57 | } 58 | 59 | func (h *masterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 60 | if !strings.Contains(r.UserAgent(), "genai-toolbox/") { 61 | h.t.Errorf("User-Agent header not found") 62 | } 63 | 64 | var body sqladmin.DatabaseInstance 65 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 66 | h.t.Fatalf("failed to decode request body: %v", err) 67 | } 68 | 69 | instanceName := body.Name 70 | if instanceName == "" { 71 | http.Error(w, "missing instance name", http.StatusBadRequest) 72 | return 73 | } 74 | 75 | var expectedBody sqladmin.DatabaseInstance 76 | var response any 77 | var statusCode int 78 | 79 | switch instanceName { 80 | case "instance1": 81 | expectedBody = sqladmin.DatabaseInstance{ 82 | Project: "p1", 83 | Name: "instance1", 84 | DatabaseVersion: "MYSQL_8_0", 85 | RootPassword: "password123", 86 | Settings: &sqladmin.Settings{ 87 | AvailabilityType: "REGIONAL", 88 | Edition: "ENTERPRISE_PLUS", 89 | Tier: "db-perf-optimized-N-8", 90 | DataDiskSizeGb: 250, 91 | DataDiskType: "PD_SSD", 92 | }, 93 | } 94 | response = map[string]any{"name": "op1", "status": "PENDING"} 95 | statusCode = http.StatusOK 96 | case "instance2": 97 | expectedBody = sqladmin.DatabaseInstance{ 98 | Project: "p2", 99 | Name: "instance2", 100 | DatabaseVersion: "MYSQL_8_4", 101 | RootPassword: "password456", 102 | Settings: &sqladmin.Settings{ 103 | AvailabilityType: "ZONAL", 104 | Edition: "ENTERPRISE_PLUS", 105 | Tier: "db-perf-optimized-N-2", 106 | DataDiskSizeGb: 100, 107 | DataDiskType: "PD_SSD", 108 | }, 109 | } 110 | response = map[string]any{"name": "op2", "status": "RUNNING"} 111 | statusCode = http.StatusOK 112 | default: 113 | http.Error(w, fmt.Sprintf("unhandled instance name: %s", instanceName), http.StatusInternalServerError) 114 | return 115 | } 116 | 117 | if expectedBody.Project != body.Project { 118 | h.t.Errorf("unexpected project: got %q, want %q", body.Project, expectedBody.Project) 119 | } 120 | if expectedBody.Name != body.Name { 121 | h.t.Errorf("unexpected name: got %q, want %q", body.Name, expectedBody.Name) 122 | } 123 | if expectedBody.DatabaseVersion != body.DatabaseVersion { 124 | h.t.Errorf("unexpected databaseVersion: got %q, want %q", body.DatabaseVersion, expectedBody.DatabaseVersion) 125 | } 126 | if expectedBody.RootPassword != body.RootPassword { 127 | h.t.Errorf("unexpected rootPassword: got %q, want %q", body.RootPassword, expectedBody.RootPassword) 128 | } 129 | if diff := cmp.Diff(expectedBody.Settings, body.Settings); diff != "" { 130 | h.t.Errorf("unexpected request body settings (-want +got):\n%s", diff) 131 | } 132 | 133 | w.Header().Set("Content-Type", "application/json") 134 | w.WriteHeader(statusCode) 135 | if err := json.NewEncoder(w).Encode(response); err != nil { 136 | http.Error(w, err.Error(), http.StatusInternalServerError) 137 | } 138 | } 139 | 140 | func TestCreateInstanceToolEndpoints(t *testing.T) { 141 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 142 | defer cancel() 143 | 144 | handler := &masterHandler{t: t} 145 | server := httptest.NewServer(handler) 146 | defer server.Close() 147 | 148 | serverURL, err := url.Parse(server.URL) 149 | if err != nil { 150 | t.Fatalf("failed to parse server URL: %v", err) 151 | } 152 | 153 | originalTransport := http.DefaultClient.Transport 154 | if originalTransport == nil { 155 | originalTransport = http.DefaultTransport 156 | } 157 | http.DefaultClient.Transport = &createInstanceTransport{ 158 | transport: originalTransport, 159 | url: serverURL, 160 | } 161 | t.Cleanup(func() { 162 | http.DefaultClient.Transport = originalTransport 163 | }) 164 | 165 | var args []string 166 | toolsFile := getCreateInstanceToolsConfig() 167 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 168 | if err != nil { 169 | t.Fatalf("command initialization returned an error: %s", err) 170 | } 171 | defer cleanup() 172 | 173 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 174 | defer cancel() 175 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 176 | if err != nil { 177 | t.Logf("toolbox command logs: \n%s", out) 178 | t.Fatalf("toolbox didn't start successfully: %s", err) 179 | } 180 | 181 | tcs := []struct { 182 | name string 183 | toolName string 184 | body string 185 | want string 186 | expectError bool 187 | errorStatus int 188 | }{ 189 | { 190 | name: "successful creation - production", 191 | toolName: "create-instance-prod", 192 | body: `{"project": "p1", "name": "instance1", "databaseVersion": "MYSQL_8_0", "rootPassword": "password123", "editionPreset": "Production"}`, 193 | want: `{"name":"op1","status":"PENDING"}`, 194 | }, 195 | { 196 | name: "successful creation - development", 197 | toolName: "create-instance-dev", 198 | body: `{"project": "p2", "name": "instance2", "rootPassword": "password456", "editionPreset": "Development"}`, 199 | want: `{"name":"op2","status":"RUNNING"}`, 200 | }, 201 | { 202 | name: "missing required parameter", 203 | toolName: "create-instance-prod", 204 | body: `{"name": "instance1"}`, 205 | expectError: true, 206 | errorStatus: http.StatusBadRequest, 207 | }, 208 | } 209 | 210 | for _, tc := range tcs { 211 | tc := tc 212 | t.Run(tc.name, func(t *testing.T) { 213 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 214 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 215 | if err != nil { 216 | t.Fatalf("unable to create request: %s", err) 217 | } 218 | req.Header.Add("Content-type", "application/json") 219 | resp, err := http.DefaultClient.Do(req) 220 | if err != nil { 221 | t.Fatalf("unable to send request: %s", err) 222 | } 223 | defer resp.Body.Close() 224 | 225 | if tc.expectError { 226 | if resp.StatusCode != tc.errorStatus { 227 | bodyBytes, _ := io.ReadAll(resp.Body) 228 | t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) 229 | } 230 | return 231 | } 232 | 233 | if resp.StatusCode != http.StatusOK { 234 | bodyBytes, _ := io.ReadAll(resp.Body) 235 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 236 | } 237 | 238 | var result struct { 239 | Result string `json:"result"` 240 | } 241 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 242 | t.Fatalf("failed to decode response: %v", err) 243 | } 244 | 245 | var got, want map[string]any 246 | if err := json.Unmarshal([]byte(result.Result), &got); err != nil { 247 | t.Fatalf("failed to unmarshal result: %v", err) 248 | } 249 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 250 | t.Fatalf("failed to unmarshal want: %v", err) 251 | } 252 | 253 | if !reflect.DeepEqual(got, want) { 254 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 255 | } 256 | }) 257 | } 258 | } 259 | 260 | func getCreateInstanceToolsConfig() map[string]any { 261 | return map[string]any{ 262 | "sources": map[string]any{ 263 | "my-cloud-sql-source": map[string]any{ 264 | "kind": "cloud-sql-admin", 265 | }, 266 | }, 267 | "tools": map[string]any{ 268 | "create-instance-prod": map[string]any{ 269 | "kind": createInstanceToolKind, 270 | "source": "my-cloud-sql-source", 271 | }, 272 | "create-instance-dev": map[string]any{ 273 | "kind": createInstanceToolKind, 274 | "source": "my-cloud-sql-source", 275 | }, 276 | }, 277 | } 278 | } 279 | ``` -------------------------------------------------------------------------------- /tests/cloudsqlmssql/cloud_sql_mssql_create_instance_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudsqlmssql_test 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/google/go-cmp/cmp" 33 | "github.com/googleapis/genai-toolbox/internal/testutils" 34 | "github.com/googleapis/genai-toolbox/tests" 35 | "google.golang.org/api/sqladmin/v1" 36 | ) 37 | 38 | var ( 39 | createInstanceToolKind = "cloud-sql-mssql-create-instance" 40 | ) 41 | 42 | type createInstanceTransport struct { 43 | transport http.RoundTripper 44 | url *url.URL 45 | } 46 | 47 | func (t *createInstanceTransport) RoundTrip(req *http.Request) (*http.Response, error) { 48 | if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { 49 | req.URL.Scheme = t.url.Scheme 50 | req.URL.Host = t.url.Host 51 | } 52 | return t.transport.RoundTrip(req) 53 | } 54 | 55 | type masterHandler struct { 56 | t *testing.T 57 | } 58 | 59 | func (h *masterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 60 | if !strings.Contains(r.UserAgent(), "genai-toolbox/") { 61 | h.t.Errorf("User-Agent header not found") 62 | } 63 | var body sqladmin.DatabaseInstance 64 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 65 | h.t.Fatalf("failed to decode request body: %v", err) 66 | } 67 | 68 | instanceName := body.Name 69 | if instanceName == "" { 70 | http.Error(w, "missing instance name", http.StatusBadRequest) 71 | return 72 | } 73 | 74 | var expectedBody sqladmin.DatabaseInstance 75 | var response any 76 | var statusCode int 77 | 78 | switch instanceName { 79 | case "instance1": 80 | expectedBody = sqladmin.DatabaseInstance{ 81 | Project: "p1", 82 | Name: "instance1", 83 | DatabaseVersion: "SQLSERVER_2022_ENTERPRISE", 84 | RootPassword: "password123", 85 | Settings: &sqladmin.Settings{ 86 | AvailabilityType: "REGIONAL", 87 | Edition: "ENTERPRISE", 88 | Tier: "db-custom-4-26624", 89 | DataDiskSizeGb: 250, 90 | DataDiskType: "PD_SSD", 91 | }, 92 | } 93 | response = map[string]any{"name": "op1", "status": "PENDING"} 94 | statusCode = http.StatusOK 95 | case "instance2": 96 | expectedBody = sqladmin.DatabaseInstance{ 97 | Project: "p2", 98 | Name: "instance2", 99 | DatabaseVersion: "SQLSERVER_2022_STANDARD", 100 | RootPassword: "password456", 101 | Settings: &sqladmin.Settings{ 102 | AvailabilityType: "ZONAL", 103 | Edition: "ENTERPRISE", 104 | Tier: "db-custom-2-8192", 105 | DataDiskSizeGb: 100, 106 | DataDiskType: "PD_SSD", 107 | }, 108 | } 109 | response = map[string]any{"name": "op2", "status": "RUNNING"} 110 | statusCode = http.StatusOK 111 | default: 112 | http.Error(w, fmt.Sprintf("unhandled instance name: %s", instanceName), http.StatusInternalServerError) 113 | return 114 | } 115 | 116 | if expectedBody.Project != body.Project { 117 | h.t.Errorf("unexpected project: got %q, want %q", body.Project, expectedBody.Project) 118 | } 119 | if expectedBody.Name != body.Name { 120 | h.t.Errorf("unexpected name: got %q, want %q", body.Name, expectedBody.Name) 121 | } 122 | if expectedBody.DatabaseVersion != body.DatabaseVersion { 123 | h.t.Errorf("unexpected databaseVersion: got %q, want %q", body.DatabaseVersion, expectedBody.DatabaseVersion) 124 | } 125 | if expectedBody.RootPassword != body.RootPassword { 126 | h.t.Errorf("unexpected rootPassword: got %q, want %q", body.RootPassword, expectedBody.RootPassword) 127 | } 128 | if diff := cmp.Diff(expectedBody.Settings, body.Settings); diff != "" { 129 | h.t.Errorf("unexpected request body settings (-want +got):\n%s", diff) 130 | } 131 | 132 | w.Header().Set("Content-Type", "application/json") 133 | w.WriteHeader(statusCode) 134 | if err := json.NewEncoder(w).Encode(response); err != nil { 135 | http.Error(w, err.Error(), http.StatusInternalServerError) 136 | } 137 | } 138 | 139 | func TestCreateInstanceToolEndpoints(t *testing.T) { 140 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 141 | defer cancel() 142 | 143 | handler := &masterHandler{t: t} 144 | server := httptest.NewServer(handler) 145 | defer server.Close() 146 | 147 | serverURL, err := url.Parse(server.URL) 148 | if err != nil { 149 | t.Fatalf("failed to parse server URL: %v", err) 150 | } 151 | 152 | originalTransport := http.DefaultClient.Transport 153 | if originalTransport == nil { 154 | originalTransport = http.DefaultTransport 155 | } 156 | http.DefaultClient.Transport = &createInstanceTransport{ 157 | transport: originalTransport, 158 | url: serverURL, 159 | } 160 | t.Cleanup(func() { 161 | http.DefaultClient.Transport = originalTransport 162 | }) 163 | 164 | var args []string 165 | toolsFile := getCreateInstanceToolsConfig() 166 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 167 | if err != nil { 168 | t.Fatalf("command initialization returned an error: %s", err) 169 | } 170 | defer cleanup() 171 | 172 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 173 | defer cancel() 174 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 175 | if err != nil { 176 | t.Logf("toolbox command logs: \n%s", out) 177 | t.Fatalf("toolbox didn't start successfully: %s", err) 178 | } 179 | 180 | tcs := []struct { 181 | name string 182 | toolName string 183 | body string 184 | want string 185 | expectError bool 186 | errorStatus int 187 | }{ 188 | { 189 | name: "successful creation - production", 190 | toolName: "create-instance-prod", 191 | body: `{"project": "p1", "name": "instance1", "databaseVersion": "SQLSERVER_2022_ENTERPRISE", "rootPassword": "password123", "editionPreset": "Production"}`, 192 | want: `{"name":"op1","status":"PENDING"}`, 193 | }, 194 | { 195 | name: "successful creation - development", 196 | toolName: "create-instance-dev", 197 | body: `{"project": "p2", "name": "instance2", "rootPassword": "password456", "editionPreset": "Development"}`, 198 | want: `{"name":"op2","status":"RUNNING"}`, 199 | }, 200 | { 201 | name: "missing required parameter", 202 | toolName: "create-instance-prod", 203 | body: `{"name": "instance1"}`, 204 | expectError: true, 205 | errorStatus: http.StatusBadRequest, 206 | }, 207 | } 208 | 209 | for _, tc := range tcs { 210 | tc := tc 211 | t.Run(tc.name, func(t *testing.T) { 212 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 213 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 214 | if err != nil { 215 | t.Fatalf("unable to create request: %s", err) 216 | } 217 | req.Header.Add("Content-type", "application/json") 218 | resp, err := http.DefaultClient.Do(req) 219 | if err != nil { 220 | t.Fatalf("unable to send request: %s", err) 221 | } 222 | defer resp.Body.Close() 223 | 224 | if tc.expectError { 225 | if resp.StatusCode != tc.errorStatus { 226 | bodyBytes, _ := io.ReadAll(resp.Body) 227 | t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) 228 | } 229 | return 230 | } 231 | 232 | if resp.StatusCode != http.StatusOK { 233 | bodyBytes, _ := io.ReadAll(resp.Body) 234 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 235 | } 236 | 237 | var result struct { 238 | Result string `json:"result"` 239 | } 240 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 241 | t.Fatalf("failed to decode response: %v", err) 242 | } 243 | 244 | var got, want map[string]any 245 | if err := json.Unmarshal([]byte(result.Result), &got); err != nil { 246 | t.Fatalf("failed to unmarshal result: %v", err) 247 | } 248 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 249 | t.Fatalf("failed to unmarshal want: %v", err) 250 | } 251 | 252 | if !reflect.DeepEqual(got, want) { 253 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 254 | } 255 | }) 256 | } 257 | } 258 | 259 | func getCreateInstanceToolsConfig() map[string]any { 260 | return map[string]any{ 261 | "sources": map[string]any{ 262 | "my-cloud-sql-source": map[string]any{ 263 | "kind": "cloud-sql-admin", 264 | }, 265 | }, 266 | "tools": map[string]any{ 267 | "create-instance-prod": map[string]any{ 268 | "kind": createInstanceToolKind, 269 | "source": "my-cloud-sql-source", 270 | }, 271 | "create-instance-dev": map[string]any{ 272 | "kind": createInstanceToolKind, 273 | "source": "my-cloud-sql-source", 274 | }, 275 | }, 276 | } 277 | } 278 | ``` -------------------------------------------------------------------------------- /tests/cloudsqlpg/cloud_sql_pg_create_instances_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudsqlpg 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/google/go-cmp/cmp" 33 | "github.com/googleapis/genai-toolbox/internal/testutils" 34 | "github.com/googleapis/genai-toolbox/tests" 35 | "google.golang.org/api/sqladmin/v1" 36 | ) 37 | 38 | var ( 39 | createInstanceToolKind = "cloud-sql-postgres-create-instance" 40 | ) 41 | 42 | type createInstanceTransport struct { 43 | transport http.RoundTripper 44 | url *url.URL 45 | } 46 | 47 | func (t *createInstanceTransport) RoundTrip(req *http.Request) (*http.Response, error) { 48 | if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { 49 | req.URL.Scheme = t.url.Scheme 50 | req.URL.Host = t.url.Host 51 | } 52 | return t.transport.RoundTrip(req) 53 | } 54 | 55 | type masterHandler struct { 56 | t *testing.T 57 | } 58 | 59 | func (h *masterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 60 | ua := r.Header.Get("User-Agent") 61 | if !strings.Contains(ua, "genai-toolbox/") { 62 | h.t.Errorf("User-Agent header not found in %q", ua) 63 | } 64 | 65 | var body sqladmin.DatabaseInstance 66 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 67 | h.t.Fatalf("failed to decode request body: %v", err) 68 | } 69 | 70 | instanceName := body.Name 71 | if instanceName == "" { 72 | http.Error(w, "missing instance name", http.StatusBadRequest) 73 | return 74 | } 75 | 76 | var expectedBody sqladmin.DatabaseInstance 77 | var response any 78 | var statusCode int 79 | 80 | switch instanceName { 81 | case "instance1": 82 | expectedBody = sqladmin.DatabaseInstance{ 83 | Project: "p1", 84 | Name: "instance1", 85 | DatabaseVersion: "POSTGRES_15", 86 | RootPassword: "password123", 87 | Settings: &sqladmin.Settings{ 88 | AvailabilityType: "REGIONAL", 89 | Edition: "ENTERPRISE_PLUS", 90 | Tier: "db-perf-optimized-N-8", 91 | DataDiskSizeGb: 250, 92 | DataDiskType: "PD_SSD", 93 | }, 94 | } 95 | response = map[string]any{"name": "op1", "status": "PENDING"} 96 | statusCode = http.StatusOK 97 | case "instance2": 98 | expectedBody = sqladmin.DatabaseInstance{ 99 | Project: "p2", 100 | Name: "instance2", 101 | DatabaseVersion: "POSTGRES_17", 102 | RootPassword: "password456", 103 | Settings: &sqladmin.Settings{ 104 | AvailabilityType: "ZONAL", 105 | Edition: "ENTERPRISE_PLUS", 106 | Tier: "db-perf-optimized-N-2", 107 | DataDiskSizeGb: 100, 108 | DataDiskType: "PD_SSD", 109 | }, 110 | } 111 | response = map[string]any{"name": "op2", "status": "RUNNING"} 112 | statusCode = http.StatusOK 113 | default: 114 | http.Error(w, fmt.Sprintf("unhandled instance name: %s", instanceName), http.StatusInternalServerError) 115 | return 116 | } 117 | 118 | if expectedBody.Project != body.Project { 119 | h.t.Errorf("unexpected project: got %q, want %q", body.Project, expectedBody.Project) 120 | } 121 | if expectedBody.Name != body.Name { 122 | h.t.Errorf("unexpected name: got %q, want %q", body.Name, expectedBody.Name) 123 | } 124 | if expectedBody.DatabaseVersion != body.DatabaseVersion { 125 | h.t.Errorf("unexpected databaseVersion: got %q, want %q", body.DatabaseVersion, expectedBody.DatabaseVersion) 126 | } 127 | if expectedBody.RootPassword != body.RootPassword { 128 | h.t.Errorf("unexpected rootPassword: got %q, want %q", body.RootPassword, expectedBody.RootPassword) 129 | } 130 | if diff := cmp.Diff(expectedBody.Settings, body.Settings); diff != "" { 131 | h.t.Errorf("unexpected request body settings (-want +got):\n%s", diff) 132 | } 133 | 134 | w.Header().Set("Content-Type", "application/json") 135 | w.WriteHeader(statusCode) 136 | if err := json.NewEncoder(w).Encode(response); err != nil { 137 | http.Error(w, err.Error(), http.StatusInternalServerError) 138 | } 139 | } 140 | 141 | func TestCreateInstanceToolEndpoints(t *testing.T) { 142 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 143 | defer cancel() 144 | 145 | handler := &masterHandler{t: t} 146 | server := httptest.NewServer(handler) 147 | defer server.Close() 148 | 149 | serverURL, err := url.Parse(server.URL) 150 | if err != nil { 151 | t.Fatalf("failed to parse server URL: %v", err) 152 | } 153 | 154 | originalTransport := http.DefaultClient.Transport 155 | if originalTransport == nil { 156 | originalTransport = http.DefaultTransport 157 | } 158 | http.DefaultClient.Transport = &createInstanceTransport{ 159 | transport: originalTransport, 160 | url: serverURL, 161 | } 162 | t.Cleanup(func() { 163 | http.DefaultClient.Transport = originalTransport 164 | }) 165 | 166 | var args []string 167 | toolsFile := getCreateInstanceToolsConfig() 168 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 169 | if err != nil { 170 | t.Fatalf("command initialization returned an error: %s", err) 171 | } 172 | defer cleanup() 173 | 174 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 175 | defer cancel() 176 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 177 | if err != nil { 178 | t.Logf("toolbox command logs: \n%s", out) 179 | t.Fatalf("toolbox didn't start successfully: %s", err) 180 | } 181 | 182 | tcs := []struct { 183 | name string 184 | toolName string 185 | body string 186 | want string 187 | expectError bool 188 | errorStatus int 189 | }{ 190 | { 191 | name: "successful creation - production", 192 | toolName: "create-instance-prod", 193 | body: `{"project": "p1", "name": "instance1", "databaseVersion": "POSTGRES_15", "rootPassword": "password123", "editionPreset": "Production"}`, 194 | want: `{"name":"op1","status":"PENDING"}`, 195 | }, 196 | { 197 | name: "successful creation - development", 198 | toolName: "create-instance-dev", 199 | body: `{"project": "p2", "name": "instance2", "rootPassword": "password456", "editionPreset": "Development"}`, 200 | want: `{"name":"op2","status":"RUNNING"}`, 201 | }, 202 | { 203 | name: "missing required parameter", 204 | toolName: "create-instance-prod", 205 | body: `{"name": "instance1"}`, 206 | expectError: true, 207 | errorStatus: http.StatusBadRequest, 208 | }, 209 | } 210 | 211 | for _, tc := range tcs { 212 | tc := tc 213 | t.Run(tc.name, func(t *testing.T) { 214 | api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) 215 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) 216 | if err != nil { 217 | t.Fatalf("unable to create request: %s", err) 218 | } 219 | req.Header.Add("Content-type", "application/json") 220 | resp, err := http.DefaultClient.Do(req) 221 | if err != nil { 222 | t.Fatalf("unable to send request: %s", err) 223 | } 224 | defer resp.Body.Close() 225 | 226 | if tc.expectError { 227 | if resp.StatusCode != tc.errorStatus { 228 | bodyBytes, _ := io.ReadAll(resp.Body) 229 | t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) 230 | } 231 | return 232 | } 233 | 234 | if resp.StatusCode != http.StatusOK { 235 | bodyBytes, _ := io.ReadAll(resp.Body) 236 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) 237 | } 238 | 239 | var result struct { 240 | Result string `json:"result"` 241 | } 242 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 243 | t.Fatalf("failed to decode response: %v", err) 244 | } 245 | 246 | var got, want map[string]any 247 | if err := json.Unmarshal([]byte(result.Result), &got); err != nil { 248 | t.Fatalf("failed to unmarshal result: %v", err) 249 | } 250 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil { 251 | t.Fatalf("failed to unmarshal want: %v", err) 252 | } 253 | 254 | if !reflect.DeepEqual(got, want) { 255 | t.Fatalf("unexpected result: got %+v, want %+v", got, want) 256 | } 257 | }) 258 | } 259 | } 260 | 261 | func getCreateInstanceToolsConfig() map[string]any { 262 | return map[string]any{ 263 | "sources": map[string]any{ 264 | "my-cloud-sql-source": map[string]any{ 265 | "kind": "cloud-sql-admin", 266 | }, 267 | }, 268 | "tools": map[string]any{ 269 | "create-instance-prod": map[string]any{ 270 | "kind": createInstanceToolKind, 271 | "source": "my-cloud-sql-source", 272 | }, 273 | "create-instance-dev": map[string]any{ 274 | "kind": createInstanceToolKind, 275 | "source": "my-cloud-sql-source", 276 | }, 277 | }, 278 | } 279 | } 280 | ``` -------------------------------------------------------------------------------- /docs/en/how-to/connect-ide/sqlite_mcp.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: SQLite using MCP 3 | type: docs 4 | weight: 2 5 | description: "Connect your IDE to SQLite using Toolbox." 6 | --- 7 | 8 | [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is 9 | an open protocol for connecting Large Language Models (LLMs) to data sources 10 | like SQLite. This guide covers how to use [MCP Toolbox for Databases][toolbox] 11 | to expose your developer assistant tools to a SQLite instance: 12 | 13 | * [Cursor][cursor] 14 | * [Windsurf][windsurf] (Codium) 15 | * [Visual Studio Code][vscode] (Copilot) 16 | * [Cline][cline] (VS Code extension) 17 | * [Claude desktop][claudedesktop] 18 | * [Claude code][claudecode] 19 | * [Gemini CLI][geminicli] 20 | * [Gemini Code Assist][geminicodeassist] 21 | 22 | [toolbox]: https://github.com/googleapis/genai-toolbox 23 | [cursor]: #configure-your-mcp-client 24 | [windsurf]: #configure-your-mcp-client 25 | [vscode]: #configure-your-mcp-client 26 | [cline]: #configure-your-mcp-client 27 | [claudedesktop]: #configure-your-mcp-client 28 | [claudecode]: #configure-your-mcp-client 29 | [geminicli]: #configure-your-mcp-client 30 | [geminicodeassist]: #configure-your-mcp-client 31 | 32 | ## Set up the database 33 | 34 | 1. [Create or select a SQLite database file.](https://www.sqlite.org/download.html) 35 | 36 | ## Install MCP Toolbox 37 | 38 | 1. Download the latest version of Toolbox as a binary. Select the [correct 39 | binary](https://github.com/googleapis/genai-toolbox/releases) corresponding 40 | to your OS and CPU architecture. You are required to use Toolbox version 41 | V0.10.0+: 42 | 43 | <!-- {x-release-please-start-version} --> 44 | {{< tabpane persist=header >}} 45 | {{< tab header="linux/amd64" lang="bash" >}} 46 | curl -O https://storage.googleapis.com/genai-toolbox/v0.18.0/linux/amd64/toolbox 47 | {{< /tab >}} 48 | 49 | {{< tab header="darwin/arm64" lang="bash" >}} 50 | curl -O https://storage.googleapis.com/genai-toolbox/v0.18.0/darwin/arm64/toolbox 51 | {{< /tab >}} 52 | 53 | {{< tab header="darwin/amd64" lang="bash" >}} 54 | curl -O https://storage.googleapis.com/genai-toolbox/v0.18.0/darwin/amd64/toolbox 55 | {{< /tab >}} 56 | 57 | {{< tab header="windows/amd64" lang="bash" >}} 58 | curl -O https://storage.googleapis.com/genai-toolbox/v0.18.0/windows/amd64/toolbox.exe 59 | {{< /tab >}} 60 | {{< /tabpane >}} 61 | <!-- {x-release-please-end} --> 62 | 63 | 1. Make the binary executable: 64 | 65 | ```bash 66 | chmod +x toolbox 67 | ``` 68 | 69 | 1. Verify the installation: 70 | 71 | ```bash 72 | ./toolbox --version 73 | ``` 74 | 75 | ## Configure your MCP Client 76 | 77 | {{< tabpane text=true >}} 78 | {{% tab header="Claude code" lang="en" %}} 79 | 80 | 1. Install [Claude 81 | Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview). 82 | 1. Create a `.mcp.json` file in your project root if it doesn't exist. 83 | 1. Add the following configuration, replace the environment variables with your 84 | values, and save: 85 | 86 | ```json 87 | { 88 | "mcpServers": { 89 | "sqlite": { 90 | "command": "./PATH/TO/toolbox", 91 | "args": ["--prebuilt", "sqlite", "--stdio"], 92 | "env": { 93 | "SQLITE_DATABASE": "./sample.db" 94 | } 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | 1. Restart Claude code to apply the new configuration. 101 | {{% /tab %}} 102 | {{% tab header="Claude desktop" lang="en" %}} 103 | 104 | 1. Open [Claude desktop](https://claude.ai/download) and navigate to Settings. 105 | 1. Under the Developer tab, tap Edit Config to open the configuration file. 106 | 1. Add the following configuration, replace the environment variables with your 107 | values, and save: 108 | 109 | ```json 110 | { 111 | "mcpServers": { 112 | "sqlite": { 113 | "command": "./PATH/TO/toolbox", 114 | "args": ["--prebuilt", "sqlite", "--stdio"], 115 | "env": { 116 | "SQLITE_DATABASE": "./sample.db" 117 | } 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | 1. Restart Claude desktop. 124 | 1. From the new chat screen, you should see a hammer (MCP) icon appear with the 125 | new MCP server available. 126 | {{% /tab %}} 127 | {{% tab header="Cline" lang="en" %}} 128 | 129 | 1. Open the [Cline](https://github.com/cline/cline) extension in VS Code and 130 | tap the **MCP Servers** icon. 131 | 1. Tap Configure MCP Servers to open the configuration file. 132 | 1. Add the following configuration, replace the environment variables with your 133 | values, and save: 134 | 135 | ```json 136 | { 137 | "mcpServers": { 138 | "sqlite": { 139 | "command": "./PATH/TO/toolbox", 140 | "args": ["--prebuilt", "sqlite", "--stdio"], 141 | "env": { 142 | "SQLITE_DATABASE": "./sample.db" 143 | } 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | 1. You should see a green active status after the server is successfully 150 | connected. 151 | {{% /tab %}} 152 | {{% tab header="Cursor" lang="en" %}} 153 | 154 | 1. Create a `.cursor` directory in your project root if it doesn't exist. 155 | 1. Create a `.cursor/mcp.json` file if it doesn't exist and open it. 156 | 1. Add the following configuration, replace the environment variables with your 157 | values, and save: 158 | 159 | ```json 160 | { 161 | "mcpServers": { 162 | "sqlite": { 163 | "command": "./PATH/TO/toolbox", 164 | "args": ["--prebuilt", "sqlite", "--stdio"], 165 | "env": { 166 | "SQLITE_DATABASE": "./sample.db" 167 | } 168 | } 169 | } 170 | } 171 | ``` 172 | 173 | 1. Open [Cursor](https://www.cursor.com/) and navigate to **Settings > Cursor 174 | Settings > MCP**. You should see a green active status after the server is 175 | successfully connected. 176 | {{% /tab %}} 177 | {{% tab header="Visual Studio Code (Copilot)" lang="en" %}} 178 | 179 | 1. Open [VS Code](https://code.visualstudio.com/docs/copilot/overview) and 180 | create a `.vscode` directory in your project root if it doesn't exist. 181 | 1. Create a `.vscode/mcp.json` file if it doesn't exist and open it. 182 | 1. Add the following configuration, replace the environment variables with your 183 | values, and save: 184 | 185 | ```json 186 | { 187 | "servers": { 188 | "sqlite": { 189 | "command": "./PATH/TO/toolbox", 190 | "args": ["--prebuilt","sqlite","--stdio"], 191 | "env": { 192 | "SQLITE_DATABASE": "./sample.db" 193 | } 194 | } 195 | } 196 | } 197 | ``` 198 | {{% /tab %}} 199 | {{% tab header="Windsurf" lang="en" %}} 200 | 201 | 1. Open [Windsurf](https://docs.codeium.com/windsurf) and navigate to the 202 | Cascade assistant. 203 | 1. Tap on the hammer (MCP) icon, then Configure to open the configuration file. 204 | 1. Add the following configuration, replace the environment variables with your 205 | values, and save: 206 | 207 | ```json 208 | { 209 | "mcpServers": { 210 | "sqlite": { 211 | "command": "./PATH/TO/toolbox", 212 | "args": ["--prebuilt","sqlite","--stdio"], 213 | "env": { 214 | "SQLITE_DATABASE": "./sample.db" 215 | } 216 | } 217 | } 218 | } 219 | ``` 220 | {{% /tab %}} 221 | {{% tab header="Gemini CLI" lang="en" %}} 222 | 223 | 1. Install the [Gemini 224 | CLI](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quickstart). 225 | 1. In your working directory, create a folder named `.gemini`. Within it, 226 | create a `settings.json` file. 227 | 1. Add the following configuration, replace the environment variables with your 228 | values, and then save: 229 | 230 | ```json 231 | { 232 | "mcpServers": { 233 | "sqlite": { 234 | "command": "./PATH/TO/toolbox", 235 | "args": ["--prebuilt","sqlite","--stdio"], 236 | "env": { 237 | "SQLITE_DATABASE": "./sample.db" 238 | } 239 | } 240 | } 241 | } 242 | ``` 243 | {{% /tab %}} 244 | {{% tab header="Gemini Code Assist" lang="en" %}} 245 | 246 | 1. Install the [Gemini Code 247 | Assist](https://marketplace.visualstudio.com/items?itemName=Google.geminicodeassist) 248 | extension in Visual Studio Code. 249 | 1. Enable Agent Mode in Gemini Code Assist chat. 250 | 1. In your working directory, create a folder named `.gemini`. Within it, 251 | create a `settings.json` file. 252 | 1. Add the following configuration, replace the environment variables with your 253 | values, and then save: 254 | 255 | ```json 256 | { 257 | "mcpServers": { 258 | "sqlite": { 259 | "command": "./PATH/TO/toolbox", 260 | "args": ["--prebuilt","sqlite","--stdio"], 261 | "env": { 262 | "SQLITE_DATABASE": "./sample.db" 263 | } 264 | } 265 | } 266 | } 267 | ``` 268 | {{% /tab %}} 269 | {{< /tabpane >}} 270 | 271 | ## Use Tools 272 | 273 | Your AI tool is now connected to SQLite using MCP. Try asking your AI assistant 274 | to list tables, create a table, or define and execute other SQL statements. 275 | 276 | The following tools are available to the LLM: 277 | 278 | 1. **list_tables**: lists tables and descriptions 279 | 1. **execute_sql**: execute any SQL statement 280 | 281 | {{< notice note >}} 282 | Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs 283 | will adapt to the tools available, so this shouldn't affect most users. 284 | {{< /notice >}} 285 | ``` -------------------------------------------------------------------------------- /tests/oracle/oracle_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright © 2025, Oracle and/or its affiliates. 2 | 3 | package oracle 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | "fmt" 9 | "os" 10 | "regexp" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/google/uuid" 16 | "github.com/googleapis/genai-toolbox/internal/testutils" 17 | "github.com/googleapis/genai-toolbox/tests" 18 | ) 19 | 20 | var ( 21 | OracleSourceKind = "oracle" 22 | OracleToolKind = "oracle-sql" 23 | OracleHost = os.Getenv("ORACLE_HOST") 24 | OracleUser = os.Getenv("ORACLE_USER") 25 | OraclePass = os.Getenv("ORACLE_PASS") 26 | OracleServerName = os.Getenv("ORACLE_SERVER_NAME") 27 | OracleConnStr = fmt.Sprintf( 28 | "%s:%s/%s", OracleHost, "1521", OracleServerName) 29 | ) 30 | 31 | func getOracleVars(t *testing.T) map[string]any { 32 | switch "" { 33 | case OracleHost: 34 | t.Fatal("'ORACLE_HOST not set") 35 | case OracleUser: 36 | t.Fatal("'ORACLE_USER' not set") 37 | case OraclePass: 38 | t.Fatal("'ORACLE_PASS' not set") 39 | case OracleServerName: 40 | t.Fatal("'ORACLE_SERVER_NAME' not set") 41 | } 42 | 43 | return map[string]any{ 44 | "kind": OracleSourceKind, 45 | "connectionString": OracleConnStr, 46 | "user": OracleUser, 47 | "password": OraclePass, 48 | } 49 | } 50 | 51 | // Copied over from oracle.go 52 | func initOracleConnection(ctx context.Context, user, pass, connStr string) (*sql.DB, error) { 53 | fullConnStr := fmt.Sprintf("oracle://%s:%s@%s", user, pass, connStr) 54 | 55 | db, err := sql.Open("oracle", fullConnStr) 56 | if err != nil { 57 | return nil, fmt.Errorf("unable to open Oracle connection: %w", err) 58 | } 59 | 60 | err = db.PingContext(ctx) 61 | if err != nil { 62 | return nil, fmt.Errorf("unable to ping Oracle connection: %w", err) 63 | } 64 | 65 | return db, nil 66 | } 67 | 68 | func TestOracleSimpleToolEndpoints(t *testing.T) { 69 | sourceConfig := getOracleVars(t) 70 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 71 | defer cancel() 72 | 73 | var args []string 74 | 75 | db, err := initOracleConnection(ctx, OracleUser, OraclePass, OracleConnStr) 76 | if err != nil { 77 | t.Fatalf("unable to create Oracle connection pool: %s", err) 78 | } 79 | 80 | dropAllUserTables(t, ctx, db) 81 | 82 | // create table name with UUID 83 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 84 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 85 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 86 | 87 | // set up data for param tool 88 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := getOracleParamToolInfo(tableNameParam) 89 | teardownTable1 := setupOracleTable(t, ctx, db, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 90 | defer teardownTable1(t) 91 | 92 | // set up data for auth tool 93 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := getOracleAuthToolInfo(tableNameAuth) 94 | teardownTable2 := setupOracleTable(t, ctx, db, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 95 | defer teardownTable2(t) 96 | 97 | // Write config into a file and pass it to command 98 | toolsFile := tests.GetToolsConfig(sourceConfig, OracleToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 99 | toolsFile = tests.AddExecuteSqlConfig(t, toolsFile, "oracle-execute-sql") 100 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement() 101 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, OracleToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") 102 | 103 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 104 | if err != nil { 105 | t.Fatalf("command initialization returned an error: %s", err) 106 | } 107 | defer cleanup() 108 | 109 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 110 | defer cancel() 111 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 112 | if err != nil { 113 | t.Logf("toolbox command logs: \n%s", out) 114 | t.Fatalf("toolbox didn't start successfully: %s", err) 115 | } 116 | 117 | // Get configs for tests 118 | select1Want := "[{\"1\":1}]" 119 | mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: ORA-00900: invalid SQL statement\n error occur at position: 0"}],"isError":true}}` 120 | createTableStatement := `"CREATE TABLE t (id NUMBER GENERATED AS IDENTITY PRIMARY KEY, name VARCHAR2(255))"` 121 | mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"1\":1}"}]}}` 122 | 123 | // Run tests 124 | tests.RunToolGetTest(t) 125 | tests.RunToolInvokeTest(t, select1Want, 126 | tests.DisableArrayTest(), 127 | ) 128 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 129 | tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) 130 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 131 | } 132 | 133 | func setupOracleTable(t *testing.T, ctx context.Context, pool *sql.DB, createStatement, insertStatement, tableName string, params []any) func(*testing.T) { 134 | err := pool.PingContext(ctx) 135 | if err != nil { 136 | t.Fatalf("unable to connect to test database: %s", err) 137 | } 138 | 139 | // Create table 140 | _, err = pool.QueryContext(ctx, createStatement) 141 | if err != nil { 142 | t.Fatalf("unable to create test table %s: %s", tableName, err) 143 | } 144 | 145 | // Insert test data 146 | _, err = pool.QueryContext(ctx, insertStatement, params...) 147 | if err != nil { 148 | t.Fatalf("unable to insert test data: %s", err) 149 | } 150 | 151 | return func(t *testing.T) { 152 | // tear down test 153 | _, err = pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE %s", tableName)) 154 | if err != nil { 155 | t.Errorf("Teardown failed: %s", err) 156 | } 157 | } 158 | } 159 | 160 | func getOracleParamToolInfo(tableName string) (string, string, string, string, string, string, []any) { 161 | // Use GENERATED AS IDENTITY for auto-incrementing primary keys. 162 | // VARCHAR2 is the standard string type in Oracle. 163 | createStatement := fmt.Sprintf(`CREATE TABLE %s ("id" NUMBER GENERATED AS IDENTITY PRIMARY KEY, "name" VARCHAR2(255))`, tableName) 164 | 165 | // MODIFIED: Use a PL/SQL block for multiple inserts 166 | insertStatement := fmt.Sprintf(` 167 | BEGIN 168 | INSERT INTO %s ("name") VALUES (:1); 169 | INSERT INTO %s ("name") VALUES (:2); 170 | INSERT INTO %s ("name") VALUES (:3); 171 | INSERT INTO %s ("name") VALUES (:4); 172 | END;`, tableName, tableName, tableName, tableName) 173 | 174 | toolStatement := fmt.Sprintf(`SELECT * FROM %s WHERE "id" = :1 OR "name" = :2`, tableName) 175 | idParamStatement := fmt.Sprintf(`SELECT * FROM %s WHERE "id" = :1`, tableName) 176 | nameParamStatement := fmt.Sprintf(`SELECT * FROM %s WHERE "name" = :1`, tableName) 177 | 178 | // Oracle's equivalent for array parameters is using the 'MEMBER OF' operator 179 | // with a collection type defined in the database schema. 180 | arrayToolStatement := fmt.Sprintf(`SELECT * FROM %s WHERE "id" MEMBER OF :1 AND "name" MEMBER OF :2`, tableName) 181 | 182 | params := []any{"Alice", "Jane", "Sid", nil} 183 | 184 | return createStatement, insertStatement, toolStatement, idParamStatement, nameParamStatement, arrayToolStatement, params 185 | } 186 | 187 | // getOracleAuthToolInfo returns statements and params for my-auth-tool for Oracle SQL 188 | func getOracleAuthToolInfo(tableName string) (string, string, string, []any) { 189 | createStatement := fmt.Sprintf(`CREATE TABLE %s ("id" NUMBER GENERATED AS IDENTITY PRIMARY KEY, "name" VARCHAR2(255), "email" VARCHAR2(255))`, tableName) 190 | 191 | // MODIFIED: Use a PL/SQL block for multiple inserts 192 | insertStatement := fmt.Sprintf(` 193 | BEGIN 194 | INSERT INTO %s ("name", "email") VALUES (:1, :2); 195 | INSERT INTO %s ("name", "email") VALUES (:3, :4); 196 | END;`, tableName, tableName) 197 | 198 | toolStatement := fmt.Sprintf(`SELECT "name" FROM %s WHERE "email" = :1`, tableName) 199 | 200 | params := []any{"Alice", tests.ServiceAccountEmail, "Jane", "[email protected]"} 201 | 202 | return createStatement, insertStatement, toolStatement, params 203 | } 204 | 205 | // dropAllUserTables finds and drops all tables owned by the current user. 206 | func dropAllUserTables(t *testing.T, ctx context.Context, db *sql.DB) { 207 | // Query for only the tables we know are created by this test suite. 208 | const query = ` 209 | SELECT table_name FROM user_tables 210 | WHERE table_name LIKE 'param_table_%' 211 | OR table_name LIKE 'auth_table_%' 212 | OR table_name LIKE 'template_param_table_%'` 213 | 214 | rows, err := db.QueryContext(ctx, query) 215 | if err != nil { 216 | t.Fatalf("failed to query for user tables: %v", err) 217 | } 218 | defer rows.Close() 219 | 220 | var tablesToDrop []string 221 | for rows.Next() { 222 | var tableName string 223 | if err := rows.Scan(&tableName); err != nil { 224 | t.Fatalf("failed to scan table name: %v", err) 225 | } 226 | tablesToDrop = append(tablesToDrop, tableName) 227 | } 228 | 229 | if err := rows.Err(); err != nil { 230 | t.Fatalf("error iterating over tables: %v", err) 231 | } 232 | 233 | for _, tableName := range tablesToDrop { 234 | _, err := db.ExecContext(ctx, fmt.Sprintf("DROP TABLE %s CASCADE CONSTRAINTS", tableName)) 235 | if err != nil { 236 | t.Logf("failed to drop table %s: %v", tableName, err) 237 | } 238 | } 239 | } 240 | ``` -------------------------------------------------------------------------------- /tests/cloudsqlpg/cloud_sql_pg_integration_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 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net" 21 | "os" 22 | "regexp" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | "cloud.google.com/go/cloudsqlconn" 28 | "github.com/google/uuid" 29 | "github.com/googleapis/genai-toolbox/internal/testutils" 30 | "github.com/googleapis/genai-toolbox/tests" 31 | "github.com/jackc/pgx/v5/pgxpool" 32 | ) 33 | 34 | var ( 35 | CloudSQLPostgresSourceKind = "cloud-sql-postgres" 36 | CloudSQLPostgresToolKind = "postgres-sql" 37 | CloudSQLPostgresProject = os.Getenv("CLOUD_SQL_POSTGRES_PROJECT") 38 | CloudSQLPostgresRegion = os.Getenv("CLOUD_SQL_POSTGRES_REGION") 39 | CloudSQLPostgresInstance = os.Getenv("CLOUD_SQL_POSTGRES_INSTANCE") 40 | CloudSQLPostgresDatabase = os.Getenv("CLOUD_SQL_POSTGRES_DATABASE") 41 | CloudSQLPostgresUser = os.Getenv("CLOUD_SQL_POSTGRES_USER") 42 | CloudSQLPostgresPass = os.Getenv("CLOUD_SQL_POSTGRES_PASS") 43 | ) 44 | 45 | func getCloudSQLPgVars(t *testing.T) map[string]any { 46 | switch "" { 47 | case CloudSQLPostgresProject: 48 | t.Fatal("'CLOUD_SQL_POSTGRES_PROJECT' not set") 49 | case CloudSQLPostgresRegion: 50 | t.Fatal("'CLOUD_SQL_POSTGRES_REGION' not set") 51 | case CloudSQLPostgresInstance: 52 | t.Fatal("'CLOUD_SQL_POSTGRES_INSTANCE' not set") 53 | case CloudSQLPostgresDatabase: 54 | t.Fatal("'CLOUD_SQL_POSTGRES_DATABASE' not set") 55 | case CloudSQLPostgresUser: 56 | t.Fatal("'CLOUD_SQL_POSTGRES_USER' not set") 57 | case CloudSQLPostgresPass: 58 | t.Fatal("'CLOUD_SQL_POSTGRES_PASS' not set") 59 | } 60 | 61 | return map[string]any{ 62 | "kind": CloudSQLPostgresSourceKind, 63 | "project": CloudSQLPostgresProject, 64 | "instance": CloudSQLPostgresInstance, 65 | "region": CloudSQLPostgresRegion, 66 | "database": CloudSQLPostgresDatabase, 67 | "user": CloudSQLPostgresUser, 68 | "password": CloudSQLPostgresPass, 69 | } 70 | } 71 | 72 | // Copied over from cloud_sql_pg.go 73 | func initCloudSQLPgConnectionPool(project, region, instance, ip_type, user, pass, dbname string) (*pgxpool.Pool, error) { 74 | // Configure the driver to connect to the database 75 | dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, pass, dbname) 76 | config, err := pgxpool.ParseConfig(dsn) 77 | if err != nil { 78 | return nil, fmt.Errorf("unable to parse connection uri: %w", err) 79 | } 80 | 81 | // Create a new dialer with options 82 | dialOpts, err := tests.GetCloudSQLDialOpts(ip_type) 83 | if err != nil { 84 | return nil, err 85 | } 86 | d, err := cloudsqlconn.NewDialer(context.Background(), cloudsqlconn.WithDefaultDialOptions(dialOpts...)) 87 | if err != nil { 88 | return nil, fmt.Errorf("unable to parse connection uri: %w", err) 89 | } 90 | 91 | // Tell the driver to use the Cloud SQL Go Connector to create connections 92 | i := fmt.Sprintf("%s:%s:%s", project, region, instance) 93 | config.ConnConfig.DialFunc = func(ctx context.Context, _ string, instance string) (net.Conn, error) { 94 | return d.Dial(ctx, i) 95 | } 96 | 97 | // Interact with the driver directly as you normally would 98 | pool, err := pgxpool.NewWithConfig(context.Background(), config) 99 | if err != nil { 100 | return nil, err 101 | } 102 | return pool, nil 103 | } 104 | 105 | func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) { 106 | sourceConfig := getCloudSQLPgVars(t) 107 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 108 | defer cancel() 109 | 110 | var args []string 111 | 112 | pool, err := initCloudSQLPgConnectionPool(CloudSQLPostgresProject, CloudSQLPostgresRegion, CloudSQLPostgresInstance, "public", CloudSQLPostgresUser, CloudSQLPostgresPass, CloudSQLPostgresDatabase) 113 | if err != nil { 114 | t.Fatalf("unable to create Cloud SQL connection pool: %s", err) 115 | } 116 | 117 | // create table name with UUID 118 | tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 119 | tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 120 | tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "") 121 | 122 | // set up data for param tool 123 | createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam) 124 | teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams) 125 | defer teardownTable1(t) 126 | 127 | // set up data for auth tool 128 | createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetPostgresSQLAuthToolInfo(tableNameAuth) 129 | teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams) 130 | defer teardownTable2(t) 131 | 132 | // Write config into a file and pass it to command 133 | toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLPostgresToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt) 134 | toolsFile = tests.AddExecuteSqlConfig(t, toolsFile, "postgres-execute-sql") 135 | tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement() 136 | toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLPostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "") 137 | 138 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 139 | if err != nil { 140 | t.Fatalf("command initialization returned an error: %s", err) 141 | } 142 | defer cleanup() 143 | 144 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 145 | defer cancel() 146 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 147 | if err != nil { 148 | t.Logf("toolbox command logs: \n%s", out) 149 | t.Fatalf("toolbox didn't start successfully: %s", err) 150 | } 151 | 152 | // Get configs for tests 153 | select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetPostgresWants() 154 | 155 | // Run tests 156 | tests.RunToolGetTest(t) 157 | tests.RunToolInvokeTest(t, select1Want) 158 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) 159 | tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) 160 | tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) 161 | } 162 | 163 | // Test connection with different IP type 164 | func TestCloudSQLPgIpConnection(t *testing.T) { 165 | sourceConfig := getCloudSQLPgVars(t) 166 | 167 | tcs := []struct { 168 | name string 169 | ipType string 170 | }{ 171 | { 172 | name: "public ip", 173 | ipType: "public", 174 | }, 175 | { 176 | name: "private ip", 177 | ipType: "private", 178 | }, 179 | } 180 | for _, tc := range tcs { 181 | t.Run(tc.name, func(t *testing.T) { 182 | sourceConfig["ipType"] = tc.ipType 183 | err := tests.RunSourceConnectionTest(t, sourceConfig, CloudSQLPostgresToolKind) 184 | if err != nil { 185 | t.Fatalf("Connection test failure: %s", err) 186 | } 187 | }) 188 | } 189 | } 190 | 191 | func TestCloudSQLPgIAMConnection(t *testing.T) { 192 | getCloudSQLPgVars(t) 193 | // service account email used for IAM should trim the suffix 194 | serviceAccountEmail := strings.TrimSuffix(tests.ServiceAccountEmail, ".gserviceaccount.com") 195 | 196 | noPassSourceConfig := map[string]any{ 197 | "kind": CloudSQLPostgresSourceKind, 198 | "project": CloudSQLPostgresProject, 199 | "instance": CloudSQLPostgresInstance, 200 | "region": CloudSQLPostgresRegion, 201 | "database": CloudSQLPostgresDatabase, 202 | "user": serviceAccountEmail, 203 | } 204 | 205 | noUserSourceConfig := map[string]any{ 206 | "kind": CloudSQLPostgresSourceKind, 207 | "project": CloudSQLPostgresProject, 208 | "instance": CloudSQLPostgresInstance, 209 | "region": CloudSQLPostgresRegion, 210 | "database": CloudSQLPostgresDatabase, 211 | "password": "random", 212 | } 213 | 214 | noUserNoPassSourceConfig := map[string]any{ 215 | "kind": CloudSQLPostgresSourceKind, 216 | "project": CloudSQLPostgresProject, 217 | "instance": CloudSQLPostgresInstance, 218 | "region": CloudSQLPostgresRegion, 219 | "database": CloudSQLPostgresDatabase, 220 | } 221 | tcs := []struct { 222 | name string 223 | sourceConfig map[string]any 224 | isErr bool 225 | }{ 226 | { 227 | name: "no user no pass", 228 | sourceConfig: noUserNoPassSourceConfig, 229 | isErr: false, 230 | }, 231 | { 232 | name: "no password", 233 | sourceConfig: noPassSourceConfig, 234 | isErr: false, 235 | }, 236 | { 237 | name: "no user", 238 | sourceConfig: noUserSourceConfig, 239 | isErr: true, 240 | }, 241 | } 242 | for _, tc := range tcs { 243 | t.Run(tc.name, func(t *testing.T) { 244 | err := tests.RunSourceConnectionTest(t, tc.sourceConfig, CloudSQLPostgresToolKind) 245 | if err != nil { 246 | if tc.isErr { 247 | return 248 | } 249 | t.Fatalf("Connection test failure: %s", err) 250 | } 251 | if tc.isErr { 252 | t.Fatalf("Expected error but test passed.") 253 | } 254 | }) 255 | } 256 | } 257 | ``` -------------------------------------------------------------------------------- /.ci/versioned.release.cloudbuild.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Copyright 2024 Google LLC 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | steps: 15 | - id: "build-docker" 16 | name: "gcr.io/cloud-builders/docker" 17 | waitFor: ['-'] 18 | script: | 19 | #!/usr/bin/env bash 20 | export VERSION=$(cat ./cmd/version.txt) 21 | docker buildx create --name container-builder --driver docker-container --bootstrap --use 22 | 23 | export TAGS="-t ${_DOCKER_URI}:$VERSION" 24 | if [[ $_PUSH_LATEST == 'true' ]]; then 25 | export TAGS="$TAGS -t ${_DOCKER_URI}:latest" 26 | fi 27 | docker buildx build --platform linux/amd64,linux/arm64 --build-arg BUILD_TYPE=container.release --build-arg COMMIT_SHA=$(git rev-parse --short HEAD) $TAGS --push . 28 | 29 | - id: "install-dependencies" 30 | name: golang:1 31 | waitFor: ['-'] 32 | env: 33 | - 'GOPATH=/gopath' 34 | volumes: 35 | - name: 'go' 36 | path: '/gopath' 37 | script: | 38 | go get -d ./... 39 | 40 | - id: "build-linux-amd64" 41 | name: golang:1 42 | waitFor: 43 | - "install-dependencies" 44 | env: 45 | - 'GOPATH=/gopath' 46 | volumes: 47 | - name: 'go' 48 | path: '/gopath' 49 | script: | 50 | #!/usr/bin/env bash 51 | export VERSION=$(cat ./cmd/version.txt) 52 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 53 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.linux.amd64 54 | 55 | - id: "store-linux-amd64" 56 | name: "gcr.io/cloud-builders/gcloud:latest" 57 | waitFor: 58 | - "build-linux-amd64" 59 | script: | 60 | #!/usr/bin/env bash 61 | export VERSION=v$(cat ./cmd/version.txt) 62 | gcloud storage cp toolbox.linux.amd64 gs://$_BUCKET_NAME/$VERSION/linux/amd64/toolbox 63 | 64 | - id: "build-linux-amd64-geminicli" 65 | name: golang:1 66 | waitFor: 67 | - "install-dependencies" 68 | env: 69 | - 'GOPATH=/gopath' 70 | volumes: 71 | - name: 'go' 72 | path: '/gopath' 73 | script: | 74 | #!/usr/bin/env bash 75 | export VERSION=$(cat ./cmd/version.txt) 76 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 77 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=geminicli.binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.geminicli.linux.amd64 78 | 79 | - id: "store-linux-amd64-geminicli" 80 | name: "gcr.io/cloud-builders/gcloud:latest" 81 | waitFor: 82 | - "build-linux-amd64-geminicli" 83 | script: | 84 | #!/usr/bin/env bash 85 | export VERSION=v$(cat ./cmd/version.txt) 86 | gcloud storage cp toolbox.geminicli.linux.amd64 gs://$_BUCKET_NAME/geminicli/$VERSION/linux/amd64/toolbox 87 | 88 | - id: "build-darwin-arm64" 89 | name: golang:1 90 | waitFor: 91 | - "install-dependencies" 92 | env: 93 | - 'GOPATH=/gopath' 94 | volumes: 95 | - name: 'go' 96 | path: '/gopath' 97 | script: | 98 | #!/usr/bin/env bash 99 | export VERSION=$(cat ./cmd/version.txt) 100 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \ 101 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.darwin.arm64 102 | 103 | - id: "store-darwin-arm64" 104 | name: "gcr.io/cloud-builders/gcloud:latest" 105 | waitFor: 106 | - "build-darwin-arm64" 107 | script: | 108 | #!/usr/bin/env bash 109 | export VERSION=v$(cat ./cmd/version.txt) 110 | gcloud storage cp toolbox.darwin.arm64 gs://$_BUCKET_NAME/$VERSION/darwin/arm64/toolbox 111 | 112 | - id: "build-darwin-arm64-geminicli" 113 | name: golang:1 114 | waitFor: 115 | - "install-dependencies" 116 | env: 117 | - 'GOPATH=/gopath' 118 | volumes: 119 | - name: 'go' 120 | path: '/gopath' 121 | script: | 122 | #!/usr/bin/env bash 123 | export VERSION=$(cat ./cmd/version.txt) 124 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \ 125 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=geminicli.binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.geminicli.darwin.arm64 126 | 127 | - id: "store-darwin-arm64-geminicli" 128 | name: "gcr.io/cloud-builders/gcloud:latest" 129 | waitFor: 130 | - "build-darwin-arm64-geminicli" 131 | script: | 132 | #!/usr/bin/env bash 133 | export VERSION=v$(cat ./cmd/version.txt) 134 | gcloud storage cp toolbox.geminicli.darwin.arm64 gs://$_BUCKET_NAME/geminicli/$VERSION/darwin/arm64/toolbox 135 | 136 | - id: "build-darwin-amd64" 137 | name: golang:1 138 | waitFor: 139 | - "install-dependencies" 140 | env: 141 | - 'GOPATH=/gopath' 142 | volumes: 143 | - name: 'go' 144 | path: '/gopath' 145 | script: | 146 | #!/usr/bin/env bash 147 | export VERSION=$(cat ./cmd/version.txt) 148 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 \ 149 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.darwin.amd64 150 | 151 | - id: "store-darwin-amd64" 152 | name: "gcr.io/cloud-builders/gcloud:latest" 153 | waitFor: 154 | - "build-darwin-amd64" 155 | script: | 156 | #!/usr/bin/env bash 157 | export VERSION=v$(cat ./cmd/version.txt) 158 | gcloud storage cp toolbox.darwin.amd64 gs://$_BUCKET_NAME/$VERSION/darwin/amd64/toolbox 159 | 160 | - id: "build-darwin-amd64-geminicli" 161 | name: golang:1 162 | waitFor: 163 | - "install-dependencies" 164 | env: 165 | - 'GOPATH=/gopath' 166 | volumes: 167 | - name: 'go' 168 | path: '/gopath' 169 | script: | 170 | #!/usr/bin/env bash 171 | export VERSION=$(cat ./cmd/version.txt) 172 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 \ 173 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=geminicli.binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.geminicli.darwin.amd64 174 | 175 | - id: "store-darwin-amd64-geminicli" 176 | name: "gcr.io/cloud-builders/gcloud:latest" 177 | waitFor: 178 | - "build-darwin-amd64-geminicli" 179 | script: | 180 | #!/usr/bin/env bash 181 | export VERSION=v$(cat ./cmd/version.txt) 182 | gcloud storage cp toolbox.geminicli.darwin.amd64 gs://$_BUCKET_NAME/geminicli/$VERSION/darwin/amd64/toolbox 183 | 184 | - id: "build-windows-amd64" 185 | name: golang:1 186 | waitFor: 187 | - "install-dependencies" 188 | env: 189 | - 'GOPATH=/gopath' 190 | volumes: 191 | - name: 'go' 192 | path: '/gopath' 193 | script: | 194 | #!/usr/bin/env bash 195 | export VERSION=$(cat ./cmd/version.txt) 196 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \ 197 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.windows.amd64 198 | 199 | - id: "store-windows-amd64" 200 | name: "gcr.io/cloud-builders/gcloud:latest" 201 | waitFor: 202 | - "build-windows-amd64" 203 | script: | 204 | #!/usr/bin/env bash 205 | export VERSION=v$(cat ./cmd/version.txt) 206 | gcloud storage cp toolbox.windows.amd64 gs://$_BUCKET_NAME/$VERSION/windows/amd64/toolbox.exe 207 | 208 | - id: "build-windows-amd64-geminicli" 209 | name: golang:1 210 | waitFor: 211 | - "install-dependencies" 212 | env: 213 | - 'GOPATH=/gopath' 214 | volumes: 215 | - name: 'go' 216 | path: '/gopath' 217 | script: | 218 | #!/usr/bin/env bash 219 | export VERSION=$(cat ./cmd/version.txt) 220 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \ 221 | go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=geminicli.binary -X github.com/googleapis/genai-toolbox/cmd.commitSha=$(git rev-parse --short HEAD)" -o toolbox.geminicli.windows.amd64 222 | 223 | - id: "store-windows-amd64-geminicli" 224 | name: "gcr.io/cloud-builders/gcloud:latest" 225 | waitFor: 226 | - "build-windows-amd64-geminicli" 227 | script: | 228 | #!/usr/bin/env bash 229 | export VERSION=v$(cat ./cmd/version.txt) 230 | gcloud storage cp toolbox.geminicli.windows.amd64 gs://$_BUCKET_NAME/geminicli/$VERSION/windows/amd64/toolbox.exe 231 | 232 | options: 233 | automapSubstitutions: true 234 | dynamicSubstitutions: true 235 | logging: CLOUD_LOGGING_ONLY # Necessary for custom service account 236 | machineType: 'E2_HIGHCPU_32' 237 | 238 | substitutions: 239 | _REGION: us-central1 240 | _AR_HOSTNAME: ${_REGION}-docker.pkg.dev 241 | _AR_REPO_NAME: toolbox 242 | _BUCKET_NAME: genai-toolbox 243 | _DOCKER_URI: ${_AR_HOSTNAME}/${PROJECT_ID}/${_AR_REPO_NAME}/toolbox 244 | _PUSH_LATEST: "true" 245 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/firestore/firestore-add-documents.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "firestore-add-documents" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "firestore-add-documents" tool adds document to a given collection path. 7 | aliases: 8 | - /resources/tools/firestore-add-documents 9 | --- 10 | ## Description 11 | 12 | The `firestore-add-documents` tool allows you to add new documents to a 13 | Firestore collection. It supports all Firestore data types using Firestore's 14 | native JSON format. The tool automatically generates a unique document ID for 15 | each new document. 16 | 17 | ## Parameters 18 | 19 | | Parameter | Type | Required | Description | 20 | |------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 21 | | `collectionPath` | string | Yes | The path of the collection where the document will be added | 22 | | `documentData` | map | Yes | The data to be added as a document to the given collection. Must use [Firestore's native JSON format](https://cloud.google.com/firestore/docs/reference/rest/Shared.Types/ArrayValue#Value) with typed values | 23 | | `returnData` | boolean | No | If set to true, the output will include the data of the created document. Defaults to false to help avoid overloading the context | 24 | 25 | ## Output 26 | 27 | The tool returns a map containing: 28 | 29 | | Field | Type | Description | 30 | |----------------|--------|--------------------------------------------------------------------------------------------------------------------------------| 31 | | `documentPath` | string | The full resource name of the created document (e.g., `projects/{projectId}/databases/{databaseId}/documents/{document_path}`) | 32 | | `createTime` | string | The timestamp when the document was created | 33 | | `documentData` | map | The data that was added (only included when `returnData` is true) | 34 | 35 | ## Data Type Format 36 | 37 | The tool requires Firestore's native JSON format for document data. Each field 38 | must be wrapped with its type indicator: 39 | 40 | ### Basic Types 41 | - **String**: `{"stringValue": "your string"}` 42 | - **Integer**: `{"integerValue": "123"}` or `{"integerValue": 123}` 43 | - **Double**: `{"doubleValue": 123.45}` 44 | - **Boolean**: `{"booleanValue": true}` 45 | - **Null**: `{"nullValue": null}` 46 | - **Bytes**: `{"bytesValue": "base64EncodedString"}` 47 | - **Timestamp**: `{"timestampValue": "2025-01-07T10:00:00Z"}` (RFC3339 format) 48 | 49 | ### Complex Types 50 | - **GeoPoint**: `{"geoPointValue": {"latitude": 34.052235, "longitude": -118.243683}}` 51 | - **Array**: `{"arrayValue": {"values": [{"stringValue": "item1"}, {"integerValue": "2"}]}}` 52 | - **Map**: `{"mapValue": {"fields": {"key1": {"stringValue": "value1"}, "key2": {"booleanValue": true}}}}` 53 | - **Reference**: `{"referenceValue": "collection/document"}` 54 | 55 | ## Examples 56 | 57 | ### Basic Document Creation 58 | 59 | ```yaml 60 | tools: 61 | add-company-doc: 62 | kind: firestore-add-documents 63 | source: my-firestore 64 | description: Add a new company document 65 | ``` 66 | 67 | Usage: 68 | ```json 69 | { 70 | "collectionPath": "companies", 71 | "documentData": { 72 | "name": { 73 | "stringValue": "Acme Corporation" 74 | }, 75 | "establishmentDate": { 76 | "timestampValue": "2000-01-15T10:30:00Z" 77 | }, 78 | "location": { 79 | "geoPointValue": { 80 | "latitude": 34.052235, 81 | "longitude": -118.243683 82 | } 83 | }, 84 | "active": { 85 | "booleanValue": true 86 | }, 87 | "employeeCount": { 88 | "integerValue": "1500" 89 | }, 90 | "annualRevenue": { 91 | "doubleValue": 1234567.89 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | ### With Nested Maps and Arrays 98 | 99 | ```json 100 | { 101 | "collectionPath": "companies", 102 | "documentData": { 103 | "name": { 104 | "stringValue": "Tech Innovations Inc" 105 | }, 106 | "contactInfo": { 107 | "mapValue": { 108 | "fields": { 109 | "email": { 110 | "stringValue": "[email protected]" 111 | }, 112 | "phone": { 113 | "stringValue": "+1-555-123-4567" 114 | }, 115 | "address": { 116 | "mapValue": { 117 | "fields": { 118 | "street": { 119 | "stringValue": "123 Innovation Drive" 120 | }, 121 | "city": { 122 | "stringValue": "San Francisco" 123 | }, 124 | "state": { 125 | "stringValue": "CA" 126 | }, 127 | "zipCode": { 128 | "stringValue": "94105" 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "products": { 137 | "arrayValue": { 138 | "values": [ 139 | { 140 | "stringValue": "Product A" 141 | }, 142 | { 143 | "stringValue": "Product B" 144 | }, 145 | { 146 | "mapValue": { 147 | "fields": { 148 | "productName": { 149 | "stringValue": "Product C Premium" 150 | }, 151 | "version": { 152 | "integerValue": "3" 153 | }, 154 | "features": { 155 | "arrayValue": { 156 | "values": [ 157 | { 158 | "stringValue": "Advanced Analytics" 159 | }, 160 | { 161 | "stringValue": "Real-time Sync" 162 | } 163 | ] 164 | } 165 | } 166 | } 167 | } 168 | } 169 | ] 170 | } 171 | } 172 | }, 173 | "returnData": true 174 | } 175 | ``` 176 | 177 | ### Complete Example with All Data Types 178 | 179 | ```json 180 | { 181 | "collectionPath": "test-documents", 182 | "documentData": { 183 | "stringField": { 184 | "stringValue": "Hello World" 185 | }, 186 | "integerField": { 187 | "integerValue": "42" 188 | }, 189 | "doubleField": { 190 | "doubleValue": 3.14159 191 | }, 192 | "booleanField": { 193 | "booleanValue": true 194 | }, 195 | "nullField": { 196 | "nullValue": null 197 | }, 198 | "timestampField": { 199 | "timestampValue": "2025-01-07T15:30:00Z" 200 | }, 201 | "geoPointField": { 202 | "geoPointValue": { 203 | "latitude": 37.7749, 204 | "longitude": -122.4194 205 | } 206 | }, 207 | "bytesField": { 208 | "bytesValue": "SGVsbG8gV29ybGQh" 209 | }, 210 | "arrayField": { 211 | "arrayValue": { 212 | "values": [ 213 | { 214 | "stringValue": "item1" 215 | }, 216 | { 217 | "integerValue": "2" 218 | }, 219 | { 220 | "booleanValue": false 221 | } 222 | ] 223 | } 224 | }, 225 | "mapField": { 226 | "mapValue": { 227 | "fields": { 228 | "nestedString": { 229 | "stringValue": "nested value" 230 | }, 231 | "nestedNumber": { 232 | "doubleValue": 99.99 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | ``` 240 | 241 | ## Authentication 242 | 243 | The tool can be configured to require authentication: 244 | 245 | ```yaml 246 | tools: 247 | secure-add-docs: 248 | kind: firestore-add-documents 249 | source: prod-firestore 250 | description: Add documents with authentication required 251 | authRequired: 252 | - google-oauth 253 | - api-key 254 | ``` 255 | 256 | ## Error Handling 257 | 258 | Common errors include: 259 | 260 | - Invalid collection path 261 | - Missing or invalid document data 262 | - Permission denied (if Firestore security rules block the operation) 263 | - Invalid data type conversions 264 | 265 | ## Best Practices 266 | 267 | 1. **Always use typed values**: Every field must be wrapped with its appropriate 268 | type indicator (e.g., `{"stringValue": "text"}`) 269 | 2. **Integer values can be strings**: The tool accepts integer values as strings 270 | (e.g., `{"integerValue": "1500"}`) 271 | 3. **Use returnData sparingly**: Only set to true when you need to verify the 272 | exact data that was written 273 | 4. **Validate data before sending**: Ensure your data matches Firestore's native 274 | JSON format 275 | 5. **Handle timestamps properly**: Use RFC3339 format for timestamp strings 276 | 6. **Base64 encode binary data**: Binary data must be base64 encoded in the 277 | `bytesValue` field 278 | 7. **Consider security rules**: Ensure your Firestore security rules allow 279 | document creation in the target collection 280 | 281 | ## Related Tools 282 | 283 | - [`firestore-get-documents`](firestore-get-documents.md) - Retrieve documents 284 | by their paths 285 | - [`firestore-query-collection`](firestore-query-collection.md) - Query 286 | documents in a collection 287 | - [`firestore-delete-documents`](firestore-delete-documents.md) - Delete 288 | documents from Firestore 289 | ``` -------------------------------------------------------------------------------- /internal/prebuiltconfigs/tools/oceanbase.yaml: -------------------------------------------------------------------------------- ```yaml 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 | sources: 16 | oceanbase-source: 17 | kind: oceanbase 18 | host: ${OCEANBASE_HOST} 19 | port: ${OCEANBASE_PORT} 20 | database: ${OCEANBASE_DATABASE} 21 | user: ${OCEANBASE_USER} 22 | password: ${OCEANBASE_PASSWORD} 23 | tools: 24 | execute_sql: 25 | kind: oceanbase-execute-sql 26 | source: oceanbase-source 27 | description: Use this tool to execute SQL. 28 | list_tables: 29 | kind: oceanbase-sql 30 | source: oceanbase-source 31 | description: "Lists detailed schema information (object type, columns, constraints, indexes, triggers, comment) as JSON for user-created tables (ordinary or partitioned). Filters by a comma-separated list of names. If names are omitted, lists all tables in user schemas." 32 | statement: | 33 | SELECT 34 | T.TABLE_SCHEMA AS schema_name, 35 | T.TABLE_NAME AS object_name, 36 | CONVERT( JSON_OBJECT( 37 | 'schema_name', T.TABLE_SCHEMA, 38 | 'object_name', T.TABLE_NAME, 39 | 'object_type', 'TABLE', 40 | 'owner', ( 41 | SELECT 42 | IFNULL(U.GRANTEE, 'N/A') 43 | FROM 44 | INFORMATION_SCHEMA.SCHEMA_PRIVILEGES U 45 | WHERE 46 | U.TABLE_SCHEMA = T.TABLE_SCHEMA 47 | LIMIT 1 48 | ), 49 | 'comment', IFNULL(T.TABLE_COMMENT, ''), 50 | 'columns', ( 51 | SELECT 52 | IFNULL( 53 | JSON_ARRAYAGG( 54 | JSON_OBJECT( 55 | 'column_name', C.COLUMN_NAME, 56 | 'data_type', C.COLUMN_TYPE, 57 | 'ordinal_position', C.ORDINAL_POSITION, 58 | 'is_not_nullable', IF(C.IS_NULLABLE = 'NO', TRUE, FALSE), 59 | 'column_default', C.COLUMN_DEFAULT, 60 | 'column_comment', IFNULL(C.COLUMN_COMMENT, '') 61 | ) 62 | ), 63 | JSON_ARRAY() 64 | ) 65 | FROM 66 | INFORMATION_SCHEMA.COLUMNS C 67 | WHERE 68 | C.TABLE_SCHEMA = T.TABLE_SCHEMA AND C.TABLE_NAME = T.TABLE_NAME 69 | ORDER BY C.ORDINAL_POSITION 70 | ), 71 | 'constraints', ( 72 | SELECT 73 | IFNULL( 74 | JSON_ARRAYAGG( 75 | JSON_OBJECT( 76 | 'constraint_name', TC.CONSTRAINT_NAME, 77 | 'constraint_type', 78 | CASE TC.CONSTRAINT_TYPE 79 | WHEN 'PRIMARY KEY' THEN 'PRIMARY KEY' 80 | WHEN 'FOREIGN KEY' THEN 'FOREIGN KEY' 81 | WHEN 'UNIQUE' THEN 'UNIQUE' 82 | ELSE TC.CONSTRAINT_TYPE 83 | END, 84 | 'constraint_definition', '', 85 | 'constraint_columns', ( 86 | SELECT 87 | IFNULL(JSON_ARRAYAGG(KCU.COLUMN_NAME), JSON_ARRAY()) 88 | FROM 89 | INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU 90 | WHERE 91 | KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA 92 | AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME 93 | AND KCU.TABLE_NAME = TC.TABLE_NAME 94 | ORDER BY KCU.ORDINAL_POSITION 95 | ), 96 | 'foreign_key_referenced_table', IF(TC.CONSTRAINT_TYPE = 'FOREIGN KEY', RC.REFERENCED_TABLE_NAME, NULL), 97 | 'foreign_key_referenced_columns', IF(TC.CONSTRAINT_TYPE = 'FOREIGN KEY', 98 | (SELECT IFNULL(JSON_ARRAYAGG(FKCU.REFERENCED_COLUMN_NAME), JSON_ARRAY()) 99 | FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE FKCU 100 | WHERE FKCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA 101 | AND FKCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME 102 | AND FKCU.TABLE_NAME = TC.TABLE_NAME 103 | AND FKCU.REFERENCED_TABLE_NAME IS NOT NULL 104 | ORDER BY FKCU.ORDINAL_POSITION), 105 | NULL 106 | ) 107 | ) 108 | ), 109 | JSON_ARRAY() 110 | ) 111 | FROM 112 | INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC 113 | LEFT JOIN 114 | INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC 115 | ON TC.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 116 | AND TC.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 117 | AND TC.TABLE_NAME = RC.TABLE_NAME 118 | WHERE 119 | TC.TABLE_SCHEMA = T.TABLE_SCHEMA AND TC.TABLE_NAME = T.TABLE_NAME 120 | ), 121 | 'indexes', ( 122 | SELECT 123 | IFNULL( 124 | JSON_ARRAYAGG( 125 | JSON_OBJECT( 126 | 'index_name', IndexData.INDEX_NAME, 127 | 'is_unique', IF(IndexData.NON_UNIQUE = 0, TRUE, FALSE), 128 | 'is_primary', IF(IndexData.INDEX_NAME = 'PRIMARY', TRUE, FALSE), 129 | 'index_columns', IFNULL(IndexData.INDEX_COLUMNS_ARRAY, JSON_ARRAY()) 130 | ) 131 | ), 132 | JSON_ARRAY() 133 | ) 134 | FROM ( 135 | SELECT 136 | S.TABLE_SCHEMA, 137 | S.TABLE_NAME, 138 | S.INDEX_NAME, 139 | MIN(S.NON_UNIQUE) AS NON_UNIQUE, -- Aggregate NON_UNIQUE here to get unique status for the index 140 | JSON_ARRAYAGG(S.COLUMN_NAME) AS INDEX_COLUMNS_ARRAY -- Aggregate columns into an array for this index 141 | FROM 142 | INFORMATION_SCHEMA.STATISTICS S 143 | WHERE 144 | S.TABLE_SCHEMA = T.TABLE_SCHEMA AND S.TABLE_NAME = T.TABLE_NAME 145 | GROUP BY 146 | S.TABLE_SCHEMA, S.TABLE_NAME, S.INDEX_NAME 147 | ) AS IndexData 148 | ORDER BY IndexData.INDEX_NAME 149 | ), 150 | 'triggers', ( 151 | SELECT 152 | IFNULL( 153 | JSON_ARRAYAGG( 154 | JSON_OBJECT( 155 | 'trigger_name', TR.TRIGGER_NAME, 156 | 'trigger_definition', TR.ACTION_STATEMENT 157 | ) 158 | ), 159 | JSON_ARRAY() 160 | ) 161 | FROM 162 | INFORMATION_SCHEMA.TRIGGERS TR 163 | WHERE 164 | TR.EVENT_OBJECT_SCHEMA = T.TABLE_SCHEMA AND TR.EVENT_OBJECT_TABLE = T.TABLE_NAME 165 | ORDER BY TR.TRIGGER_NAME 166 | ) 167 | ) USING utf8mb4) AS object_details 168 | FROM 169 | INFORMATION_SCHEMA.TABLES T 170 | WHERE 171 | T.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') 172 | AND (NULLIF(TRIM(?), '') IS NULL OR FIND_IN_SET(T.TABLE_NAME, ?)) 173 | AND T.TABLE_TYPE = 'BASE TABLE' 174 | ORDER BY 175 | T.TABLE_SCHEMA, T.TABLE_NAME; 176 | parameters: 177 | - name: table_names 178 | type: string 179 | description: "Optional: A comma-separated list of table names. If empty, details for all tables in user-accessible schemas will be listed." 180 | toolsets: 181 | oceanbase_database_tools: 182 | - execute_sql 183 | - list_tables 184 | ``` -------------------------------------------------------------------------------- /docs/en/samples/bigquery/mcp_quickstart/_index.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Quickstart (MCP with BigQuery)" 3 | type: docs 4 | weight: 2 5 | description: > 6 | How to get started running Toolbox with MCP Inspector and BigQuery as the source. 7 | --- 8 | 9 | ## Overview 10 | 11 | [Model Context Protocol](https://modelcontextprotocol.io) is an open protocol 12 | that standardizes how applications provide context to LLMs. Check out this page 13 | on how to [connect to Toolbox via MCP](../../../how-to/connect_via_mcp.md). 14 | 15 | ## Step 1: Set up your BigQuery Dataset and Table 16 | 17 | In this section, we will create a BigQuery dataset and a table, then insert some 18 | data that needs to be accessed by our agent. 19 | 20 | 1. Create a new BigQuery dataset (replace `YOUR_DATASET_NAME` with your desired 21 | dataset name, e.g., `toolbox_mcp_ds`, and optionally specify a location like 22 | `US` or `EU`): 23 | 24 | ```bash 25 | export BQ_DATASET_NAME="YOUR_DATASET_NAME" 26 | export BQ_LOCATION="US" 27 | 28 | bq --location=$BQ_LOCATION mk $BQ_DATASET_NAME 29 | ``` 30 | 31 | You can also do this through the [Google Cloud 32 | Console](https://console.cloud.google.com/bigquery). 33 | 34 | 1. The `hotels` table needs to be defined in your new dataset. First, create a 35 | file named `create_hotels_table.sql` with the following content: 36 | 37 | ```sql 38 | CREATE TABLE IF NOT EXISTS `YOUR_PROJECT_ID.YOUR_DATASET_NAME.hotels` ( 39 | id INT64 NOT NULL, 40 | name STRING NOT NULL, 41 | location STRING NOT NULL, 42 | price_tier STRING NOT NULL, 43 | checkin_date DATE NOT NULL, 44 | checkout_date DATE NOT NULL, 45 | booked BOOLEAN NOT NULL 46 | ); 47 | ``` 48 | 49 | > **Note:** Replace `YOUR_PROJECT_ID` and `YOUR_DATASET_NAME` in the SQL 50 | > with your actual project ID and dataset name. 51 | 52 | Then run the command below to execute the sql query: 53 | 54 | ```bash 55 | bq query --project_id=$GOOGLE_CLOUD_PROJECT --dataset_id=$BQ_DATASET_NAME --use_legacy_sql=false < create_hotels_table.sql 56 | ``` 57 | 58 | 1. . Next, populate the hotels table with some initial data. To do this, create 59 | a file named `insert_hotels_data.sql` and add the following SQL INSERT 60 | statement to it. 61 | 62 | ```sql 63 | INSERT INTO `YOUR_PROJECT_ID.YOUR_DATASET_NAME.hotels` (id, name, location, price_tier, checkin_date, checkout_date, booked) 64 | VALUES 65 | (1, 'Hilton Basel', 'Basel', 'Luxury', '2024-04-20', '2024-04-22', FALSE), 66 | (2, 'Marriott Zurich', 'Zurich', 'Upscale', '2024-04-14', '2024-04-21', FALSE), 67 | (3, 'Hyatt Regency Basel', 'Basel', 'Upper Upscale', '2024-04-02', '2024-04-20', FALSE), 68 | (4, 'Radisson Blu Lucerne', 'Lucerne', 'Midscale', '2024-04-05', '2024-04-24', FALSE), 69 | (5, 'Best Western Bern', 'Bern', 'Upper Midscale', '2024-04-01', '2024-04-23', FALSE), 70 | (6, 'InterContinental Geneva', 'Geneva', 'Luxury', '2024-04-23', '2024-04-28', FALSE), 71 | (7, 'Sheraton Zurich', 'Zurich', 'Upper Upscale', '2024-04-02', '2024-04-27', FALSE), 72 | (8, 'Holiday Inn Basel', 'Basel', 'Upper Midscale', '2024-04-09', '2024-04-24', FALSE), 73 | (9, 'Courtyard Zurich', 'Zurich', 'Upscale', '2024-04-03', '2024-04-13', FALSE), 74 | (10, 'Comfort Inn Bern', 'Bern', 'Midscale', '2024-04-04', '2024-04-16', FALSE); 75 | ``` 76 | 77 | > **Note:** Replace `YOUR_PROJECT_ID` and `YOUR_DATASET_NAME` in the SQL 78 | > with your actual project ID and dataset name. 79 | 80 | Then run the command below to execute the sql query: 81 | 82 | ```bash 83 | bq query --project_id=$GOOGLE_CLOUD_PROJECT --dataset_id=$BQ_DATASET_NAME --use_legacy_sql=false < insert_hotels_data.sql 84 | ``` 85 | 86 | ## Step 2: Install and configure Toolbox 87 | 88 | In this section, we will download Toolbox, configure our tools in a 89 | `tools.yaml`, and then run the Toolbox server. 90 | 91 | 1. Download the latest version of Toolbox as a binary: 92 | 93 | {{< notice tip >}} 94 | Select the 95 | [correct binary](https://github.com/googleapis/genai-toolbox/releases) 96 | corresponding to your OS and CPU architecture. 97 | {{< /notice >}} 98 | <!-- {x-release-please-start-version} --> 99 | ```bash 100 | export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64 101 | curl -O https://storage.googleapis.com/genai-toolbox/v0.18.0/$OS/toolbox 102 | ``` 103 | <!-- {x-release-please-end} --> 104 | 105 | 1. Make the binary executable: 106 | 107 | ```bash 108 | chmod +x toolbox 109 | ``` 110 | 111 | 1. Write the following into a `tools.yaml` file. You must replace the 112 | `YOUR_PROJECT_ID` and `YOUR_DATASET_NAME` placeholder in the config with your 113 | actual BigQuery project and dataset name. The `location` field is optional; 114 | if not specified, it defaults to 'us'. The table name `hotels` is used 115 | directly in the statements. 116 | 117 | {{< notice tip >}} 118 | Authentication with BigQuery is handled via Application Default Credentials 119 | (ADC). Ensure you have run `gcloud auth application-default login`. 120 | {{< /notice >}} 121 | 122 | ```yaml 123 | sources: 124 | my-bigquery-source: 125 | kind: bigquery 126 | project: YOUR_PROJECT_ID 127 | location: us 128 | tools: 129 | search-hotels-by-name: 130 | kind: bigquery-sql 131 | source: my-bigquery-source 132 | description: Search for hotels based on name. 133 | parameters: 134 | - name: name 135 | type: string 136 | description: The name of the hotel. 137 | statement: SELECT * FROM `YOUR_DATASET_NAME.hotels` WHERE LOWER(name) LIKE LOWER(CONCAT('%', @name, '%')); 138 | search-hotels-by-location: 139 | kind: bigquery-sql 140 | source: my-bigquery-source 141 | description: Search for hotels based on location. 142 | parameters: 143 | - name: location 144 | type: string 145 | description: The location of the hotel. 146 | statement: SELECT * FROM `YOUR_DATASET_NAME.hotels` WHERE LOWER(location) LIKE LOWER(CONCAT('%', @location, '%')); 147 | book-hotel: 148 | kind: bigquery-sql 149 | source: my-bigquery-source 150 | description: >- 151 | Book a hotel by its ID. If the hotel is successfully booked, returns a NULL, raises an error if not. 152 | parameters: 153 | - name: hotel_id 154 | type: integer 155 | description: The ID of the hotel to book. 156 | statement: UPDATE `YOUR_DATASET_NAME.hotels` SET booked = TRUE WHERE id = @hotel_id; 157 | update-hotel: 158 | kind: bigquery-sql 159 | source: my-bigquery-source 160 | description: >- 161 | Update a hotel's check-in and check-out dates by its ID. Returns a message indicating whether the hotel was successfully updated or not. 162 | parameters: 163 | - name: checkin_date 164 | type: string 165 | description: The new check-in date of the hotel. 166 | - name: checkout_date 167 | type: string 168 | description: The new check-out date of the hotel. 169 | - name: hotel_id 170 | type: integer 171 | description: The ID of the hotel to update. 172 | statement: >- 173 | UPDATE `YOUR_DATASET_NAME.hotels` SET checkin_date = PARSE_DATE('%Y-%m-%d', @checkin_date), checkout_date = PARSE_DATE('%Y-%m-%d', @checkout_date) WHERE id = @hotel_id; 174 | cancel-hotel: 175 | kind: bigquery-sql 176 | source: my-bigquery-source 177 | description: Cancel a hotel by its ID. 178 | parameters: 179 | - name: hotel_id 180 | type: integer 181 | description: The ID of the hotel to cancel. 182 | statement: UPDATE `YOUR_DATASET_NAME.hotels` SET booked = FALSE WHERE id = @hotel_id; 183 | toolsets: 184 | my-toolset: 185 | - search-hotels-by-name 186 | - search-hotels-by-location 187 | - book-hotel 188 | - update-hotel 189 | - cancel-hotel 190 | ``` 191 | 192 | For more info on tools, check out the 193 | [Tools](../../../resources/tools/) section. 194 | 195 | 1. Run the Toolbox server, pointing to the `tools.yaml` file created earlier: 196 | 197 | ```bash 198 | ./toolbox --tools-file "tools.yaml" 199 | ``` 200 | 201 | ## Step 3: Connect to MCP Inspector 202 | 203 | 1. Run the MCP Inspector: 204 | 205 | ```bash 206 | npx @modelcontextprotocol/inspector 207 | ``` 208 | 209 | 1. Type `y` when it asks to install the inspector package. 210 | 211 | 1. It should show the following when the MCP Inspector is up and running (please 212 | take note of `<YOUR_SESSION_TOKEN>`): 213 | 214 | ```bash 215 | Starting MCP inspector... 216 | ⚙️ Proxy server listening on localhost:6277 217 | 🔑 Session token: <YOUR_SESSION_TOKEN> 218 | Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth 219 | 220 | 🚀 MCP Inspector is up and running at: 221 | http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<YOUR_SESSION_TOKEN> 222 | ``` 223 | 224 | 1. Open the above link in your browser. 225 | 226 | 1. For `Transport Type`, select `Streamable HTTP`. 227 | 228 | 1. For `URL`, type in `http://127.0.0.1:5000/mcp`. 229 | 230 | 1. For `Configuration` -> `Proxy Session Token`, make sure 231 | `<YOUR_SESSION_TOKEN>` is present. 232 | 233 | 1. Click Connect. 234 | 235 |  236 | 237 | 1. Select `List Tools`, you will see a list of tools configured in `tools.yaml`. 238 | 239 |  240 | 241 | 1. Test out your tools here! 242 | ``` -------------------------------------------------------------------------------- /internal/tools/sqlite/sqlitesql/sqlitesql_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 sqlitesql_test 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "reflect" 21 | "testing" 22 | 23 | yaml "github.com/goccy/go-yaml" 24 | "github.com/google/go-cmp/cmp" 25 | "github.com/googleapis/genai-toolbox/internal/server" 26 | "github.com/googleapis/genai-toolbox/internal/testutils" 27 | "github.com/googleapis/genai-toolbox/internal/tools" 28 | "github.com/googleapis/genai-toolbox/internal/tools/sqlite/sqlitesql" 29 | _ "modernc.org/sqlite" 30 | ) 31 | 32 | func TestParseFromYamlSQLite(t *testing.T) { 33 | ctx, err := testutils.ContextWithNewLogger() 34 | if err != nil { 35 | t.Fatalf("unexpected error: %s", err) 36 | } 37 | tcs := []struct { 38 | desc string 39 | in string 40 | want server.ToolConfigs 41 | }{ 42 | { 43 | desc: "basic example", 44 | in: ` 45 | tools: 46 | example_tool: 47 | kind: sqlite-sql 48 | source: my-sqlite-instance 49 | description: some description 50 | statement: | 51 | SELECT * FROM SQL_STATEMENT; 52 | authRequired: 53 | - my-google-auth-service 54 | - other-auth-service 55 | parameters: 56 | - name: country 57 | type: string 58 | description: some description 59 | authServices: 60 | - name: my-google-auth-service 61 | field: user_id 62 | - name: other-auth-service 63 | field: user_id 64 | `, 65 | want: server.ToolConfigs{ 66 | "example_tool": sqlitesql.Config{ 67 | Name: "example_tool", 68 | Kind: "sqlite-sql", 69 | Source: "my-sqlite-instance", 70 | Description: "some description", 71 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 72 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 73 | Parameters: []tools.Parameter{ 74 | tools.NewStringParameterWithAuth("country", "some description", 75 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 76 | {Name: "other-auth-service", Field: "user_id"}}), 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 | // Parse contents 88 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 89 | if err != nil { 90 | t.Fatalf("unable to unmarshal: %s", err) 91 | } 92 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 93 | t.Fatalf("incorrect parse: diff %v", diff) 94 | } 95 | }) 96 | } 97 | 98 | } 99 | 100 | func TestParseFromYamlWithTemplateSqlite(t *testing.T) { 101 | ctx, err := testutils.ContextWithNewLogger() 102 | if err != nil { 103 | t.Fatalf("unexpected error: %s", err) 104 | } 105 | tcs := []struct { 106 | desc string 107 | in string 108 | want server.ToolConfigs 109 | }{ 110 | { 111 | desc: "basic example", 112 | in: ` 113 | tools: 114 | example_tool: 115 | kind: sqlite-sql 116 | source: my-sqlite-db 117 | description: some description 118 | statement: | 119 | SELECT * FROM SQL_STATEMENT; 120 | authRequired: 121 | - my-google-auth-service 122 | - other-auth-service 123 | parameters: 124 | - name: country 125 | type: string 126 | description: some description 127 | authServices: 128 | - name: my-google-auth-service 129 | field: user_id 130 | - name: other-auth-service 131 | field: user_id 132 | templateParameters: 133 | - name: tableName 134 | type: string 135 | description: The table to select hotels from. 136 | - name: fieldArray 137 | type: array 138 | description: The columns to return for the query. 139 | items: 140 | name: column 141 | type: string 142 | description: A column name that will be returned from the query. 143 | `, 144 | want: server.ToolConfigs{ 145 | "example_tool": sqlitesql.Config{ 146 | Name: "example_tool", 147 | Kind: "sqlite-sql", 148 | Source: "my-sqlite-db", 149 | Description: "some description", 150 | Statement: "SELECT * FROM SQL_STATEMENT;\n", 151 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 152 | Parameters: []tools.Parameter{ 153 | tools.NewStringParameterWithAuth("country", "some description", 154 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, 155 | {Name: "other-auth-service", Field: "user_id"}}), 156 | }, 157 | TemplateParameters: []tools.Parameter{ 158 | tools.NewStringParameter("tableName", "The table to select hotels from."), 159 | tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")), 160 | }, 161 | }, 162 | }, 163 | }, 164 | } 165 | for _, tc := range tcs { 166 | t.Run(tc.desc, func(t *testing.T) { 167 | got := struct { 168 | Tools server.ToolConfigs `yaml:"tools"` 169 | }{} 170 | // Parse contents 171 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 172 | if err != nil { 173 | t.Fatalf("unable to unmarshal: %s", err) 174 | } 175 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 176 | t.Fatalf("incorrect parse: diff %v", diff) 177 | } 178 | }) 179 | } 180 | } 181 | 182 | func setupTestDB(t *testing.T) *sql.DB { 183 | db, err := sql.Open("sqlite", ":memory:") 184 | if err != nil { 185 | t.Fatalf("Failed to open in-memory database: %v", err) 186 | } 187 | 188 | createTable := ` 189 | CREATE TABLE users ( 190 | id INTEGER PRIMARY KEY, 191 | name TEXT, 192 | age INTEGER 193 | );` 194 | if _, err := db.Exec(createTable); err != nil { 195 | t.Fatalf("Failed to create table: %v", err) 196 | } 197 | 198 | insertData := ` 199 | INSERT INTO users (id, name, age) VALUES 200 | (1, 'Alice', 30), 201 | (2, 'Bob', 25);` 202 | if _, err := db.Exec(insertData); err != nil { 203 | t.Fatalf("Failed to insert data: %v", err) 204 | } 205 | 206 | return db 207 | } 208 | 209 | func TestTool_Invoke(t *testing.T) { 210 | type fields struct { 211 | Name string 212 | Kind string 213 | AuthRequired []string 214 | Parameters tools.Parameters 215 | TemplateParameters tools.Parameters 216 | AllParams tools.Parameters 217 | Db *sql.DB 218 | Statement string 219 | } 220 | type args struct { 221 | ctx context.Context 222 | params tools.ParamValues 223 | accessToken tools.AccessToken 224 | } 225 | tests := []struct { 226 | name string 227 | fields fields 228 | args args 229 | want any 230 | wantErr bool 231 | }{ 232 | { 233 | name: "simple select", 234 | fields: fields{ 235 | Db: setupTestDB(t), 236 | Statement: "SELECT * FROM users", 237 | }, 238 | args: args{ 239 | ctx: context.Background(), 240 | }, 241 | want: []any{ 242 | map[string]any{"id": int64(1), "name": "Alice", "age": int64(30)}, 243 | map[string]any{"id": int64(2), "name": "Bob", "age": int64(25)}, 244 | }, 245 | wantErr: false, 246 | }, 247 | { 248 | name: "select with parameter", 249 | fields: fields{ 250 | Db: setupTestDB(t), 251 | Statement: "SELECT * FROM users WHERE name = ?", 252 | Parameters: []tools.Parameter{ 253 | tools.NewStringParameter("name", "user name"), 254 | }, 255 | }, 256 | args: args{ 257 | ctx: context.Background(), 258 | params: []tools.ParamValue{ 259 | {Name: "name", Value: "Alice"}, 260 | }, 261 | }, 262 | want: []any{ 263 | map[string]any{"id": int64(1), "name": "Alice", "age": int64(30)}, 264 | }, 265 | wantErr: false, 266 | }, 267 | { 268 | name: "select with template parameter", 269 | fields: fields{ 270 | Db: setupTestDB(t), 271 | Statement: "SELECT * FROM {{.tableName}}", 272 | TemplateParameters: []tools.Parameter{ 273 | tools.NewStringParameter("tableName", "table name"), 274 | }, 275 | }, 276 | args: args{ 277 | ctx: context.Background(), 278 | params: []tools.ParamValue{ 279 | {Name: "tableName", Value: "users"}, 280 | }, 281 | }, 282 | want: []any{ 283 | map[string]any{"id": int64(1), "name": "Alice", "age": int64(30)}, 284 | map[string]any{"id": int64(2), "name": "Bob", "age": int64(25)}, 285 | }, 286 | wantErr: false, 287 | }, 288 | { 289 | name: "invalid sql", 290 | fields: fields{ 291 | Db: setupTestDB(t), 292 | Statement: "SELECT * FROM non_existent_table", 293 | }, 294 | args: args{ 295 | ctx: context.Background(), 296 | }, 297 | want: nil, 298 | wantErr: true, 299 | }, 300 | } 301 | for _, tt := range tests { 302 | t.Run(tt.name, func(t *testing.T) { 303 | tr := sqlitesql.Tool{ 304 | Name: tt.fields.Name, 305 | Kind: tt.fields.Kind, 306 | AuthRequired: tt.fields.AuthRequired, 307 | Parameters: tt.fields.Parameters, 308 | TemplateParameters: tt.fields.TemplateParameters, 309 | AllParams: tt.fields.AllParams, 310 | Db: tt.fields.Db, 311 | Statement: tt.fields.Statement, 312 | } 313 | got, err := tr.Invoke(tt.args.ctx, tt.args.params, tt.args.accessToken) 314 | if (err != nil) != tt.wantErr { 315 | t.Errorf("Tool.Invoke() error = %v, wantErr %v", err, tt.wantErr) 316 | return 317 | } 318 | if !reflect.DeepEqual(got, tt.want) { 319 | t.Errorf("Tool.Invoke() = %v, want %v", got, tt.want) 320 | } 321 | }) 322 | } 323 | } 324 | ```