This is page 9 of 45. 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-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ └── looker-run-look.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ └── lookerrunlook │ │ │ ├── lookerrunlook_test.go │ │ │ └── lookerrunlook.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /internal/server/static/js/mainContent.js: -------------------------------------------------------------------------------- ```javascript 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 | /** 16 | * Renders the main content area into the HTML. 17 | * @param {string} containerId The ID of the DOM element to inject the content into. 18 | * @param {string} idString The id of the item inside the main content area. 19 | */ 20 | function renderMainContent(containerId, idString, instructionContent) { 21 | const mainContentContainer = document.getElementById(containerId); 22 | if (!mainContentContainer) { 23 | console.error(`Content container with ID "${containerId}" not found.`); 24 | return; 25 | } 26 | 27 | const idAttribute = idString ? `id="${idString}"` : ''; 28 | const contentHTML = ` 29 | <div class="main-content-area"> 30 | <div class="top-bar"> 31 | </div> 32 | <main class="content" ${idAttribute}"> 33 | ${instructionContent} 34 | </main> 35 | </div> 36 | `; 37 | 38 | mainContentContainer.innerHTML = contentHTML; 39 | } 40 | 41 | function getHomepageInstructions() { 42 | return ` 43 | <div class="resource-instructions"> 44 | <h1 class="resource-title">Welcome to Toolbox UI</h1> 45 | <p class="resource-intro">Toolbox UI is a built-in web interface that allows users to visually inspect and test out configured resources such as tools and toolsets. To get started, select a resource from the navigation tab to the left.</p> 46 | <a href="https://googleapis.github.io/genai-toolbox/how-to/use-toolbox-ui/" class="btn btn--externalDocs" target="_blank" rel="noopener noreferrer">Toolbox UI Documentation</a> 47 | </div> 48 | `; 49 | } 50 | 51 | function getToolInstructions() { 52 | return ` 53 | <div class="resource-instructions"> 54 | <h1 class="resource-title">Tools</h1> 55 | <p class="resource-intro">To inspect and test a tool, please click on one of your tools to the left.</p> 56 | <h2 class="resource-subtitle">What are Tools?</h2> 57 | <p class="resource-description"> 58 | Tools define actions an agent can take, such as running a SQL statement or interacting with a source. 59 | You can define Tools as a map in the <code>tools</code> section of your <code>tools.yaml</code> file. <br><br> 60 | Some tools also use <strong>parameters</strong>. Parameters for each Tool will define what inputs the agent will need to provide to invoke them. 61 | </p> 62 | <a href="https://googleapis.github.io/genai-toolbox/resources/tools/" class="btn btn--externalDocs" target="_blank" rel="noopener noreferrer">Tools Documentation</a> 63 | </div> 64 | `; 65 | } 66 | 67 | function getToolsetInstructions() { 68 | return ` 69 | <div class="resource-instructions"> 70 | <h1 class="resource-title">Toolsets</h1> 71 | <p class="resource-intro">To inspect a specific toolset, please enter the name of a toolset and press search.</p> 72 | <h2 class="resource-subtitle">What are Toolsets?</h2> 73 | <p class="resource-description"> 74 | Toolsets define groups of tools an agent can access. You can define Toolsets as a map in the <code>toolsets</code> section of your <code>tools.yaml</code> file. Toolsets may 75 | only include valid tools that are also defined in your <code>tools.yaml</code> file. 76 | </p> 77 | <a href="https://googleapis.github.io/genai-toolbox/getting-started/configure/#toolsets" class="btn btn--externalDocs" target="_blank" rel="noopener noreferrer">Toolsets Documentation</a> 78 | </div> 79 | `; 80 | } ``` -------------------------------------------------------------------------------- /internal/sources/cloudsqlmysql/cloud_sql_mysql.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 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "net/url" 22 | "slices" 23 | 24 | "cloud.google.com/go/cloudsqlconn/mysql/mysql" 25 | "github.com/goccy/go-yaml" 26 | "github.com/googleapis/genai-toolbox/internal/sources" 27 | "github.com/googleapis/genai-toolbox/internal/util" 28 | "go.opentelemetry.io/otel/trace" 29 | ) 30 | 31 | const SourceKind string = "cloud-sql-mysql" 32 | 33 | // validate interface 34 | var _ sources.SourceConfig = Config{} 35 | 36 | func init() { 37 | if !sources.Register(SourceKind, newConfig) { 38 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 39 | } 40 | } 41 | 42 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 43 | actual := Config{Name: name, IPType: "public"} // Default IPType 44 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 45 | return nil, err 46 | } 47 | return actual, nil 48 | } 49 | 50 | type Config struct { 51 | Name string `yaml:"name" validate:"required"` 52 | Kind string `yaml:"kind" validate:"required"` 53 | Project string `yaml:"project" validate:"required"` 54 | Region string `yaml:"region" validate:"required"` 55 | Instance string `yaml:"instance" validate:"required"` 56 | IPType sources.IPType `yaml:"ipType"` 57 | User string `yaml:"user" validate:"required"` 58 | Password string `yaml:"password" validate:"required"` 59 | Database string `yaml:"database" validate:"required"` 60 | } 61 | 62 | func (r Config) SourceConfigKind() string { 63 | return SourceKind 64 | } 65 | 66 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 67 | pool, err := initCloudSQLMySQLConnectionPool(ctx, tracer, r.Name, r.Project, r.Region, r.Instance, r.IPType.String(), r.User, r.Password, r.Database) 68 | if err != nil { 69 | return nil, fmt.Errorf("unable to create pool: %w", err) 70 | } 71 | 72 | err = pool.PingContext(ctx) 73 | if err != nil { 74 | return nil, fmt.Errorf("unable to connect successfully: %w", err) 75 | } 76 | 77 | s := &Source{ 78 | Name: r.Name, 79 | Kind: SourceKind, 80 | Pool: pool, 81 | } 82 | return s, nil 83 | } 84 | 85 | var _ sources.Source = &Source{} 86 | 87 | type Source struct { 88 | Name string `yaml:"name"` 89 | Kind string `yaml:"kind"` 90 | Pool *sql.DB 91 | } 92 | 93 | func (s *Source) SourceKind() string { 94 | return SourceKind 95 | } 96 | 97 | func (s *Source) MySQLPool() *sql.DB { 98 | return s.Pool 99 | } 100 | 101 | func initCloudSQLMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, project, region, instance, ipType, user, pass, dbname string) (*sql.DB, error) { 102 | //nolint:all // Reassigned ctx 103 | ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name) 104 | defer span.End() 105 | 106 | // Create a new dialer with options 107 | userAgent, err := util.UserAgentFromContext(ctx) 108 | if err != nil { 109 | return nil, err 110 | } 111 | opts, err := sources.GetCloudSQLOpts(ipType, userAgent, false) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | if !slices.Contains(sql.Drivers(), "cloudsql-mysql") { 117 | _, err = mysql.RegisterDriver("cloudsql-mysql", opts...) 118 | if err != nil { 119 | return nil, fmt.Errorf("unable to register driver: %w", err) 120 | } 121 | } 122 | // Tell the driver to use the Cloud SQL Go Connector to create connections 123 | dsn := fmt.Sprintf("%s:%s@cloudsql-mysql(%s:%s:%s)/%s?connectionAttributes=program_name:%s", user, pass, project, region, instance, dbname, url.QueryEscape(userAgent)) 124 | db, err := sql.Open( 125 | "cloudsql-mysql", 126 | dsn, 127 | ) 128 | if err != nil { 129 | return nil, err 130 | } 131 | return db, nil 132 | } 133 | ``` -------------------------------------------------------------------------------- /docs/en/about/faq.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "FAQ" 3 | type: docs 4 | weight: 2 5 | description: Frequently asked questions about Toolbox. 6 | --- 7 | 8 | ## How can I deploy or run Toolbox? 9 | 10 | MCP Toolbox for Databases is open-source and can be run or deployed to a 11 | multitude of environments. For convenience, we release [compiled binaries and 12 | docker images][release-notes] (but you can always compile yourself as well!). 13 | 14 | For detailed instructions, check out these resources: 15 | 16 | - [Quickstart: How to Run Locally](../getting-started/local_quickstart.md) 17 | - [Deploy to Cloud Run](../how-to/deploy_toolbox.md) 18 | 19 | [release-notes]: https://github.com/googleapis/genai-toolbox/releases/ 20 | 21 | ## Do I need a Google Cloud account/project to get started with Toolbox? 22 | 23 | Nope! While some of the sources Toolbox connects to may require GCP credentials, 24 | Toolbox doesn't require them and can connect to a bunch of different resources 25 | that don't. 26 | 27 | ## Does Toolbox take contributions from external users? 28 | 29 | Absolutely! Please check out our [DEVELOPER.md][] for instructions on how to get 30 | started developing _on_ Toolbox instead of with it, and the [CONTRIBUTING.md][] 31 | for instructions on completing the CLA and getting a PR accepted. 32 | 33 | [DEVELOPER.md]: https://github.com/googleapis/genai-toolbox/blob/main/DEVELOPER.md 34 | [CONTRIBUTING.MD]: https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md 35 | 36 | ## Can Toolbox support a feature to let me do _$FOO_? 37 | 38 | Maybe? The best place to start is by [opening an issue][github-issue] for 39 | discussion (or seeing if there is already one open), so we can better understand 40 | your use case and the best way to solve it. Generally we aim to prioritize the 41 | most popular issues, so make sure to +1 ones you are the most interested in. 42 | 43 | [github-issue]: https://github.com/googleapis/genai-toolbox/issues 44 | 45 | ## Can Toolbox be used for non-database tools? 46 | 47 | Currently, Toolbox is primarily focused on making it easier to create and 48 | develop tools focused on interacting with Databases. We believe that there are a 49 | lot of unique problems when interacting with Databases for Gen AI use cases, and 50 | want to prioritize solving those first. 51 | 52 | However, we've also received feedback that supporting more generic HTTP or 53 | GRPC tools might be helpful in assisting with migrating to Toolbox or in 54 | accomplishing more complicated workflows. We're looking into what that might 55 | best look like in Toolbox. 56 | 57 | ## Can I use _$BAR_ orchestration framework to use tools from Toolbox? 58 | 59 | Currently, Toolbox only supports a limited number of client SDKs at our initial 60 | launch. We are investigating support for more frameworks as well as more general 61 | approaches for users without a framework -- look forward to seeing an update 62 | soon. 63 | 64 | ## Why does Toolbox use a server-client architecture pattern? 65 | 66 | Toolbox's server-client architecture allows us to more easily support a wide 67 | variety of languages and frameworks with a centralized implementation. It also 68 | allows us to tackle problems like connection pooling, auth, or caching more 69 | completely than entirely client-side solutions. 70 | 71 | ## Why was Toolbox written in Go? 72 | 73 | While a large part of the Gen AI Ecosystem is predominately Python, we opted to 74 | use Go. We chose Go because it's still easy and simple to use, but also easier 75 | to write fast, efficient, and concurrent servers. Additionally, given the 76 | server-client architecture, we can still meet many developers where they are 77 | with clients in their preferred language. As Gen AI matures, we want developers 78 | to be able to use Toolbox on the serving path of mission critical applications. 79 | It's easier to build the needed robustness, performance and scalability in Go 80 | than in Python. 81 | 82 | ## Is Toolbox compatible with Model Context Protocol (MCP)? 83 | 84 | Yes! Toolbox is compatible with [Anthropic's Model Context Protocol 85 | (MCP)](https://modelcontextprotocol.io/). Please checkout [Connect via 86 | MCP](../how-to/connect_via_mcp.md) on how to connect to Toolbox with an MCP 87 | client. 88 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/sqlite/sqlite-sql.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "sqlite-sql" 3 | type: docs 4 | weight: 1 5 | description: > 6 | Execute SQL statements against a SQLite database. 7 | aliases: 8 | - /resources/tools/sqlite-sql 9 | --- 10 | 11 | ## About 12 | 13 | A `sqlite-sql` tool executes SQL statements against a SQLite database. 14 | It's compatible with any of the following sources: 15 | 16 | - [sqlite](../../sources/sqlite.md) 17 | 18 | SQLite uses the `?` placeholder for parameters in SQL statements. Parameters are 19 | bound in the order they are provided. 20 | 21 | The statement field supports any valid SQLite SQL statement, including `SELECT`, 22 | `INSERT`, `UPDATE`, `DELETE`, `CREATE/ALTER/DROP` table statements, and other 23 | DDL statements. 24 | 25 | ### Example 26 | 27 | > **Note:** This tool uses parameterized queries to prevent SQL injections. 28 | > Query parameters can be used as substitutes for arbitrary expressions. 29 | > Parameters cannot be used as substitutes for identifiers, column names, table 30 | > names, or other parts of the query. 31 | 32 | ```yaml 33 | tools: 34 | search-users: 35 | kind: sqlite-sql 36 | source: my-sqlite-db 37 | description: Search users by name and age 38 | parameters: 39 | - name: name 40 | type: string 41 | description: The name to search for 42 | - name: min_age 43 | type: integer 44 | description: Minimum age 45 | statement: SELECT * FROM users WHERE name LIKE ? AND age >= ? 46 | ``` 47 | 48 | ### Example with Template Parameters 49 | 50 | > **Note:** This tool allows direct modifications to the SQL statement, 51 | > including identifiers, column names, and table names. **This makes it more 52 | > vulnerable to SQL injections**. Using basic parameters only (see above) is 53 | > recommended for performance and safety reasons. For more details, please check 54 | > [templateParameters](..#template-parameters). 55 | 56 | ```yaml 57 | tools: 58 | list_table: 59 | kind: sqlite-sql 60 | source: my-sqlite-db 61 | statement: | 62 | SELECT * FROM {{.tableName}}; 63 | description: | 64 | Use this tool to list all information from a specific table. 65 | Example: 66 | {{ 67 | "tableName": "flights", 68 | }} 69 | templateParameters: 70 | - name: tableName 71 | type: string 72 | description: Table to select from 73 | ``` 74 | 75 | ## Reference 76 | 77 | | **field** | **type** | **required** | **description** | 78 | |--------------------|:------------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------| 79 | | kind | string | true | Must be "sqlite-sql". | 80 | | source | string | true | Name of the source the SQLite source configuration. | 81 | | description | string | true | Description of the tool that is passed to the LLM. | 82 | | statement | string | true | The SQL statement to execute. | 83 | | parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | 84 | | templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | 85 | ``` -------------------------------------------------------------------------------- /docs/en/resources/sources/redis.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Redis" 3 | linkTitle: "Redis" 4 | type: docs 5 | weight: 1 6 | description: > 7 | Redis is a in-memory data structure store. 8 | 9 | --- 10 | 11 | ## About 12 | 13 | Redis is a in-memory data structure store, used as a database, 14 | cache, and message broker. It supports data structures such as strings, hashes, 15 | lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, and 16 | geospatial indexes with radius queries. 17 | 18 | If you are new to Redis, you can find installation and getting started guides on 19 | the [official Redis website](https://redis.io/docs/). 20 | 21 | ## Available Tools 22 | 23 | - [`redis`](../tools/redis/redis.md) 24 | Run Redis commands and interact with key-value pairs. 25 | 26 | ## Requirements 27 | 28 | ### Redis 29 | 30 | [AUTH string][auth] is a password for connection to Redis. If you have the 31 | `requirepass` directive set in your Redis configuration, incoming client 32 | connections must authenticate in order to connect. 33 | 34 | Specify your AUTH string in the password field: 35 | 36 | ```yaml 37 | sources: 38 | my-redis-instance: 39 | kind: redis 40 | address: 41 | - 127.0.0.1:6379 42 | username: ${MY_USER_NAME} 43 | password: ${MY_AUTH_STRING} # Omit this field if you don't have a password. 44 | # database: 0 45 | # clusterEnabled: false 46 | # useGCPIAM: false 47 | ``` 48 | 49 | {{< notice tip >}} 50 | Use environment variable replacement with the format ${ENV_NAME} 51 | instead of hardcoding your secrets into the configuration file. 52 | {{< /notice >}} 53 | 54 | ### Memorystore For Redis 55 | 56 | Memorystore standalone instances support authentication using an [AUTH][auth] 57 | string. 58 | 59 | Here is an example tools.yaml config with [AUTH][auth] enabled: 60 | 61 | ```yaml 62 | sources: 63 | my-redis-cluster-instance: 64 | kind: memorystore-redis 65 | address: 66 | - 127.0.0.1:6379 67 | password: ${MY_AUTH_STRING} 68 | # useGCPIAM: false 69 | # clusterEnabled: false 70 | ``` 71 | 72 | Memorystore Redis Cluster supports IAM authentication instead. Grant your 73 | account the required [IAM role][iam] and make sure to set `useGCPIAM` to `true`. 74 | 75 | Here is an example tools.yaml config for Memorystore Redis Cluster instances 76 | using IAM authentication: 77 | 78 | ```yaml 79 | sources: 80 | my-redis-cluster-instance: 81 | kind: memorystore-redis 82 | address: 83 | - 127.0.0.1:6379 84 | useGCPIAM: true 85 | clusterEnabled: true 86 | ``` 87 | 88 | [iam]: https://cloud.google.com/memorystore/docs/cluster/about-iam-auth 89 | 90 | ## Reference 91 | 92 | | **field** | **type** | **required** | **description** | 93 | |----------------|:--------:|:------------:|---------------------------------------------------------------------------------------------------------------------------------| 94 | | kind | string | true | Must be "memorystore-redis". | 95 | | address | string | true | Primary endpoint for the Memorystore Redis instance to connect to. | 96 | | username | string | false | If you are using a non-default user, specify the user name here. If you are using Memorystore for Redis, leave this field blank | 97 | | password | string | false | If you have [Redis AUTH][auth] enabled, specify the AUTH string here | 98 | | database | int | false | The Redis database to connect to. Not applicable for cluster enabled instances. The default database is `0`. | 99 | | clusterEnabled | bool | false | Set it to `true` if using a Redis Cluster instance. Defaults to `false`. | 100 | | useGCPIAM | string | false | Set it to `true` if you are using GCP's IAM authentication. Defaults to `false`. | 101 | 102 | [auth]: https://cloud.google.com/memorystore/docs/redis/about-redis-auth 103 | ``` -------------------------------------------------------------------------------- /internal/sources/valkey/valkey_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 valkey_test 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/googleapis/genai-toolbox/internal/server" 24 | "github.com/googleapis/genai-toolbox/internal/sources" 25 | "github.com/googleapis/genai-toolbox/internal/sources/valkey" 26 | "github.com/googleapis/genai-toolbox/internal/testutils" 27 | ) 28 | 29 | func TestParseFromYamlValkey(t *testing.T) { 30 | tcs := []struct { 31 | desc string 32 | in string 33 | want server.SourceConfigs 34 | }{ 35 | { 36 | desc: "default setting", 37 | in: ` 38 | sources: 39 | my-valkey-instance: 40 | kind: valkey 41 | address: 42 | - 127.0.0.1 43 | `, 44 | want: map[string]sources.SourceConfig{ 45 | "my-valkey-instance": valkey.Config{ 46 | Name: "my-valkey-instance", 47 | Kind: valkey.SourceKind, 48 | Address: []string{"127.0.0.1"}, 49 | Username: "", 50 | Password: "", 51 | Database: 0, 52 | UseGCPIAM: false, 53 | DisableCache: false, 54 | }, 55 | }, 56 | }, 57 | { 58 | desc: "advanced example", 59 | in: ` 60 | sources: 61 | my-valkey-instance: 62 | kind: valkey 63 | address: 64 | - 127.0.0.1 65 | database: 1 66 | username: user 67 | password: pass 68 | useGCPIAM: true 69 | disableCache: true 70 | `, 71 | want: map[string]sources.SourceConfig{ 72 | "my-valkey-instance": valkey.Config{ 73 | Name: "my-valkey-instance", 74 | Kind: valkey.SourceKind, 75 | Address: []string{"127.0.0.1"}, 76 | Username: "user", 77 | Password: "pass", 78 | Database: 1, 79 | UseGCPIAM: true, 80 | DisableCache: true, 81 | }, 82 | }, 83 | }, 84 | } 85 | for _, tc := range tcs { 86 | t.Run(tc.desc, func(t *testing.T) { 87 | got := struct { 88 | Sources server.SourceConfigs `yaml:"sources"` 89 | }{} 90 | // Parse contents 91 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 92 | if err != nil { 93 | t.Fatalf("unable to unmarshal: %s", err) 94 | } 95 | if !cmp.Equal(tc.want, got.Sources) { 96 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 97 | } 98 | }) 99 | } 100 | 101 | } 102 | 103 | func TestFailParseFromYaml(t *testing.T) { 104 | tcs := []struct { 105 | desc string 106 | in string 107 | err string 108 | }{ 109 | { 110 | desc: "invalid database", 111 | in: ` 112 | sources: 113 | my-valkey-instance: 114 | kind: valkey 115 | project: my-project 116 | address: 117 | - 127.0.0.1 118 | database: my-db 119 | useGCPIAM: false 120 | `, 121 | err: "cannot unmarshal string into Go struct field .Sources of type int", 122 | }, 123 | { 124 | desc: "extra field", 125 | in: ` 126 | sources: 127 | my-valkey-instance: 128 | kind: valkey 129 | address: 130 | - 127.0.0.1 131 | project: proj 132 | database: 1 133 | `, 134 | err: "unable to parse source \"my-valkey-instance\" as \"valkey\": [5:1] unknown field \"project\"", 135 | }, 136 | { 137 | desc: "missing required field", 138 | in: ` 139 | sources: 140 | my-valkey-instance: 141 | kind: valkey 142 | `, 143 | err: "unable to parse source \"my-valkey-instance\" as \"valkey\": Key: 'Config.Address' Error:Field validation for 'Address' failed on the 'required' tag", 144 | }, 145 | } 146 | for _, tc := range tcs { 147 | t.Run(tc.desc, func(t *testing.T) { 148 | got := struct { 149 | Sources server.SourceConfigs `yaml:"sources"` 150 | }{} 151 | // Parse contents 152 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 153 | if err == nil { 154 | t.Fatalf("expect parsing to fail") 155 | } 156 | errStr := err.Error() 157 | if !strings.Contains(errStr, tc.err) { 158 | t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) 159 | } 160 | }) 161 | } 162 | } 163 | ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbfind/mongodbfind_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 mongodbfind_test 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | "github.com/googleapis/genai-toolbox/internal/tools" 22 | "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfind" 23 | 24 | yaml "github.com/goccy/go-yaml" 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/googleapis/genai-toolbox/internal/server" 27 | "github.com/googleapis/genai-toolbox/internal/testutils" 28 | ) 29 | 30 | func TestParseFromYamlMongoQuery(t *testing.T) { 31 | ctx, err := testutils.ContextWithNewLogger() 32 | if err != nil { 33 | t.Fatalf("unexpected error: %s", err) 34 | } 35 | tcs := []struct { 36 | desc string 37 | in string 38 | want server.ToolConfigs 39 | }{ 40 | { 41 | desc: "basic example", 42 | in: ` 43 | tools: 44 | example_tool: 45 | kind: mongodb-find 46 | source: my-instance 47 | description: some description 48 | database: test_db 49 | collection: test_coll 50 | filterPayload: | 51 | { name: {{json .name}} } 52 | filterParams: 53 | - name: name 54 | type: string 55 | description: small description 56 | projectPayload: | 57 | { name: 1, age: 1 } 58 | projectParams: [] 59 | sortPayload: | 60 | { timestamp: -1 } 61 | sortParams: [] 62 | `, 63 | want: server.ToolConfigs{ 64 | "example_tool": mongodbfind.Config{ 65 | Name: "example_tool", 66 | Kind: "mongodb-find", 67 | Source: "my-instance", 68 | AuthRequired: []string{}, 69 | Database: "test_db", 70 | Collection: "test_coll", 71 | Description: "some description", 72 | FilterPayload: "{ name: {{json .name}} }\n", 73 | FilterParams: tools.Parameters{ 74 | &tools.StringParameter{ 75 | CommonParameter: tools.CommonParameter{ 76 | Name: "name", 77 | Type: "string", 78 | Desc: "small description", 79 | }, 80 | }, 81 | }, 82 | ProjectPayload: "{ name: 1, age: 1 }\n", 83 | ProjectParams: tools.Parameters{}, 84 | SortPayload: "{ timestamp: -1 }\n", 85 | SortParams: tools.Parameters{}, 86 | }, 87 | }, 88 | }, 89 | } 90 | for _, tc := range tcs { 91 | t.Run(tc.desc, func(t *testing.T) { 92 | got := struct { 93 | Tools server.ToolConfigs `yaml:"tools"` 94 | }{} 95 | // Parse contents 96 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 97 | if err != nil { 98 | t.Fatalf("unable to unmarshal: %s", err) 99 | } 100 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 101 | t.Fatalf("incorrect parse: diff %v", diff) 102 | } 103 | }) 104 | } 105 | 106 | } 107 | 108 | func TestFailParseFromYamlMongoQuery(t *testing.T) { 109 | ctx, err := testutils.ContextWithNewLogger() 110 | if err != nil { 111 | t.Fatalf("unexpected error: %s", err) 112 | } 113 | tcs := []struct { 114 | desc string 115 | in string 116 | err string 117 | }{ 118 | { 119 | desc: "Invalid method", 120 | in: ` 121 | tools: 122 | example_tool: 123 | kind: mongodb-find 124 | source: my-instance 125 | description: some description 126 | collection: test_coll 127 | filterPayload: | 128 | { name : {{json .name}} } 129 | `, 130 | err: `unable to parse tool "example_tool" as kind "mongodb-find"`, 131 | }, 132 | } 133 | for _, tc := range tcs { 134 | t.Run(tc.desc, func(t *testing.T) { 135 | got := struct { 136 | Tools server.ToolConfigs `yaml:"tools"` 137 | }{} 138 | // Parse contents 139 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 140 | if err == nil { 141 | t.Fatalf("expect parsing to fail") 142 | } 143 | errStr := err.Error() 144 | if !strings.Contains(errStr, tc.err) { 145 | t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) 146 | } 147 | }) 148 | } 149 | 150 | } 151 | ``` -------------------------------------------------------------------------------- /internal/tools/mongodb/mongodbfindone/mongodbfindone_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 mongodbfindone_test 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | "github.com/googleapis/genai-toolbox/internal/tools" 22 | "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfindone" 23 | 24 | yaml "github.com/goccy/go-yaml" 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/googleapis/genai-toolbox/internal/server" 27 | "github.com/googleapis/genai-toolbox/internal/testutils" 28 | ) 29 | 30 | func TestParseFromYamlMongoQuery(t *testing.T) { 31 | ctx, err := testutils.ContextWithNewLogger() 32 | if err != nil { 33 | t.Fatalf("unexpected error: %s", err) 34 | } 35 | tcs := []struct { 36 | desc string 37 | in string 38 | want server.ToolConfigs 39 | }{ 40 | { 41 | desc: "basic example", 42 | in: ` 43 | tools: 44 | example_tool: 45 | kind: mongodb-find-one 46 | source: my-instance 47 | description: some description 48 | database: test_db 49 | collection: test_coll 50 | filterPayload: | 51 | { name: {{json .name}} } 52 | filterParams: 53 | - name: name 54 | type: string 55 | description: small description 56 | projectPayload: | 57 | { name: 1, age: 1 } 58 | projectParams: [] 59 | sortPayload: | 60 | { timestamp: -1 } 61 | sortParams: [] 62 | `, 63 | want: server.ToolConfigs{ 64 | "example_tool": mongodbfindone.Config{ 65 | Name: "example_tool", 66 | Kind: "mongodb-find-one", 67 | Source: "my-instance", 68 | AuthRequired: []string{}, 69 | Database: "test_db", 70 | Collection: "test_coll", 71 | Description: "some description", 72 | FilterPayload: "{ name: {{json .name}} }\n", 73 | FilterParams: tools.Parameters{ 74 | &tools.StringParameter{ 75 | CommonParameter: tools.CommonParameter{ 76 | Name: "name", 77 | Type: "string", 78 | Desc: "small description", 79 | }, 80 | }, 81 | }, 82 | ProjectPayload: "{ name: 1, age: 1 }\n", 83 | ProjectParams: tools.Parameters{}, 84 | SortPayload: "{ timestamp: -1 }\n", 85 | SortParams: tools.Parameters{}, 86 | }, 87 | }, 88 | }, 89 | } 90 | for _, tc := range tcs { 91 | t.Run(tc.desc, func(t *testing.T) { 92 | got := struct { 93 | Tools server.ToolConfigs `yaml:"tools"` 94 | }{} 95 | // Parse contents 96 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 97 | if err != nil { 98 | t.Fatalf("unable to unmarshal: %s", err) 99 | } 100 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 101 | t.Fatalf("incorrect parse: diff %v", diff) 102 | } 103 | }) 104 | } 105 | 106 | } 107 | 108 | func TestFailParseFromYamlMongoQuery(t *testing.T) { 109 | ctx, err := testutils.ContextWithNewLogger() 110 | if err != nil { 111 | t.Fatalf("unexpected error: %s", err) 112 | } 113 | tcs := []struct { 114 | desc string 115 | in string 116 | err string 117 | }{ 118 | { 119 | desc: "Invalid method", 120 | in: ` 121 | tools: 122 | example_tool: 123 | kind: mongodb-find-one 124 | source: my-instance 125 | description: some description 126 | collection: test_coll 127 | filterPayload: | 128 | { name : {{json .name}} } 129 | `, 130 | err: `unable to parse tool "example_tool" as kind "mongodb-find-one"`, 131 | }, 132 | } 133 | for _, tc := range tcs { 134 | t.Run(tc.desc, func(t *testing.T) { 135 | got := struct { 136 | Tools server.ToolConfigs `yaml:"tools"` 137 | }{} 138 | // Parse contents 139 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 140 | if err == nil { 141 | t.Fatalf("expect parsing to fail") 142 | } 143 | errStr := err.Error() 144 | if !strings.Contains(errStr, tc.err) { 145 | t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) 146 | } 147 | }) 148 | } 149 | 150 | } 151 | ``` -------------------------------------------------------------------------------- /internal/sources/oceanbase/oceanbase_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 oceanbase_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/sources/oceanbase" 24 | "github.com/googleapis/genai-toolbox/internal/testutils" 25 | ) 26 | 27 | // Test parsing OceanBase source config from YAML. 28 | func TestParseFromYamlOceanBase(t *testing.T) { 29 | tcs := []struct { 30 | desc string 31 | in string 32 | want server.SourceConfigs 33 | }{ 34 | { 35 | desc: "basic example", 36 | in: ` 37 | sources: 38 | my-oceanbase-instance: 39 | kind: oceanbase 40 | host: 0.0.0.0 41 | port: 2881 42 | database: ob_db 43 | user: ob_user 44 | password: ob_pass 45 | `, 46 | want: server.SourceConfigs{ 47 | "my-oceanbase-instance": oceanbase.Config{ 48 | Name: "my-oceanbase-instance", 49 | Kind: oceanbase.SourceKind, 50 | Host: "0.0.0.0", 51 | Port: "2881", 52 | Database: "ob_db", 53 | User: "ob_user", 54 | Password: "ob_pass", 55 | }, 56 | }, 57 | }, 58 | { 59 | desc: "with query timeout", 60 | in: ` 61 | sources: 62 | my-oceanbase-instance: 63 | kind: oceanbase 64 | host: 0.0.0.0 65 | port: 2881 66 | database: ob_db 67 | user: ob_user 68 | password: ob_pass 69 | queryTimeout: 30s 70 | `, 71 | want: server.SourceConfigs{ 72 | "my-oceanbase-instance": oceanbase.Config{ 73 | Name: "my-oceanbase-instance", 74 | Kind: oceanbase.SourceKind, 75 | Host: "0.0.0.0", 76 | Port: "2881", 77 | Database: "ob_db", 78 | User: "ob_user", 79 | Password: "ob_pass", 80 | QueryTimeout: "30s", 81 | }, 82 | }, 83 | }, 84 | } 85 | for _, tc := range tcs { 86 | t.Run(tc.desc, func(t *testing.T) { 87 | got := struct { 88 | Sources server.SourceConfigs `yaml:"sources"` 89 | }{} 90 | // Parse contents 91 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 92 | if err != nil { 93 | t.Fatalf("unable to unmarshal: %s", err) 94 | } 95 | if !cmp.Equal(tc.want, got.Sources) { 96 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | // Test parsing failure cases for OceanBase source config. 103 | func TestFailParseFromYamlOceanBase(t *testing.T) { 104 | tcs := []struct { 105 | desc string 106 | in string 107 | err string 108 | }{ 109 | { 110 | desc: "extra field", 111 | in: ` 112 | sources: 113 | my-oceanbase-instance: 114 | kind: oceanbase 115 | host: 0.0.0.0 116 | port: 2881 117 | database: ob_db 118 | user: ob_user 119 | password: ob_pass 120 | foo: bar 121 | `, 122 | err: "unable to parse source \"my-oceanbase-instance\" as \"oceanbase\": [2:1] unknown field \"foo\"\n 1 | database: ob_db\n> 2 | foo: bar\n ^\n 3 | host: 0.0.0.0\n 4 | kind: oceanbase\n 5 | password: ob_pass\n 6 | ", 123 | }, 124 | { 125 | desc: "missing required field", 126 | in: ` 127 | sources: 128 | my-oceanbase-instance: 129 | kind: oceanbase 130 | port: 2881 131 | database: ob_db 132 | user: ob_user 133 | password: ob_pass 134 | `, 135 | err: "unable to parse source \"my-oceanbase-instance\" as \"oceanbase\": Key: 'Config.Host' Error:Field validation for 'Host' failed on the 'required' tag", 136 | }, 137 | } 138 | for _, tc := range tcs { 139 | t.Run(tc.desc, func(t *testing.T) { 140 | got := struct { 141 | Sources server.SourceConfigs `yaml:"sources"` 142 | }{} 143 | // Parse contents 144 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 145 | if err == nil { 146 | t.Fatalf("expect parsing to fail") 147 | } 148 | errStr := err.Error() 149 | if errStr != tc.err { 150 | t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) 151 | } 152 | }) 153 | } 154 | } 155 | ``` -------------------------------------------------------------------------------- /internal/sources/postgres/postgres.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 postgres 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/url" 21 | "strings" 22 | 23 | "github.com/goccy/go-yaml" 24 | "github.com/googleapis/genai-toolbox/internal/sources" 25 | "github.com/googleapis/genai-toolbox/internal/util" 26 | "github.com/jackc/pgx/v5/pgxpool" 27 | "go.opentelemetry.io/otel/trace" 28 | ) 29 | 30 | const SourceKind string = "postgres" 31 | 32 | // validate interface 33 | var _ sources.SourceConfig = Config{} 34 | 35 | func init() { 36 | if !sources.Register(SourceKind, newConfig) { 37 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 38 | } 39 | } 40 | 41 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 42 | actual := Config{Name: name} 43 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 44 | return nil, err 45 | } 46 | return actual, nil 47 | } 48 | 49 | type Config struct { 50 | Name string `yaml:"name" validate:"required"` 51 | Kind string `yaml:"kind" validate:"required"` 52 | Host string `yaml:"host" validate:"required"` 53 | Port string `yaml:"port" validate:"required"` 54 | User string `yaml:"user" validate:"required"` 55 | Password string `yaml:"password" validate:"required"` 56 | Database string `yaml:"database" validate:"required"` 57 | QueryParams map[string]string `yaml:"queryParams"` 58 | } 59 | 60 | func (r Config) SourceConfigKind() string { 61 | return SourceKind 62 | } 63 | 64 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 65 | pool, err := initPostgresConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryParams) 66 | if err != nil { 67 | return nil, fmt.Errorf("unable to create pool: %w", err) 68 | } 69 | 70 | err = pool.Ping(ctx) 71 | if err != nil { 72 | return nil, fmt.Errorf("unable to connect successfully: %w", err) 73 | } 74 | 75 | s := &Source{ 76 | Name: r.Name, 77 | Kind: SourceKind, 78 | Pool: pool, 79 | } 80 | return s, nil 81 | } 82 | 83 | var _ sources.Source = &Source{} 84 | 85 | type Source struct { 86 | Name string `yaml:"name"` 87 | Kind string `yaml:"kind"` 88 | Pool *pgxpool.Pool 89 | } 90 | 91 | func (s *Source) SourceKind() string { 92 | return SourceKind 93 | } 94 | 95 | func (s *Source) PostgresPool() *pgxpool.Pool { 96 | return s.Pool 97 | } 98 | 99 | func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string, queryParams map[string]string) (*pgxpool.Pool, error) { 100 | //nolint:all // Reassigned ctx 101 | ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name) 102 | defer span.End() 103 | userAgent, err := util.UserAgentFromContext(ctx) 104 | if err != nil { 105 | userAgent = "genai-toolbox" 106 | } 107 | if queryParams == nil { 108 | // Initialize the map before using it 109 | queryParams = make(map[string]string) 110 | } 111 | if _, ok := queryParams["application_name"]; !ok { 112 | queryParams["application_name"] = userAgent 113 | } 114 | 115 | // urlExample := "postgres:dd//username:password@localhost:5432/database_name" 116 | url := &url.URL{ 117 | Scheme: "postgres", 118 | User: url.UserPassword(user, pass), 119 | Host: fmt.Sprintf("%s:%s", host, port), 120 | Path: dbname, 121 | RawQuery: ConvertParamMapToRawQuery(queryParams), 122 | } 123 | pool, err := pgxpool.New(ctx, url.String()) 124 | if err != nil { 125 | return nil, fmt.Errorf("unable to create connection pool: %w", err) 126 | } 127 | 128 | return pool, nil 129 | } 130 | 131 | func ConvertParamMapToRawQuery(queryParams map[string]string) string { 132 | queryArray := []string{} 133 | for k, v := range queryParams { 134 | queryArray = append(queryArray, fmt.Sprintf("%s=%s", k, v)) 135 | } 136 | return strings.Join(queryArray, "&") 137 | } 138 | ``` -------------------------------------------------------------------------------- /internal/sources/cassandra/cassandra.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 cassandra 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/goccy/go-yaml" 22 | "github.com/gocql/gocql" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "go.opentelemetry.io/otel/trace" 25 | ) 26 | 27 | const SourceKind string = "cassandra" 28 | 29 | func init() { 30 | if !sources.Register(SourceKind, newConfig) { 31 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 32 | } 33 | } 34 | 35 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 36 | actual := Config{Name: name} 37 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 38 | return nil, err 39 | } 40 | return actual, nil 41 | } 42 | 43 | type Config struct { 44 | Name string `yaml:"name" validate:"required"` 45 | Kind string `yaml:"kind" validate:"required"` 46 | Hosts []string `yaml:"hosts" validate:"required"` 47 | Keyspace string `yaml:"keyspace"` 48 | ProtoVersion int `yaml:"protoVersion"` 49 | Username string `yaml:"username"` 50 | Password string `yaml:"password"` 51 | CAPath string `yaml:"caPath"` 52 | CertPath string `yaml:"certPath"` 53 | KeyPath string `yaml:"keyPath"` 54 | EnableHostVerification bool `yaml:"enableHostVerification"` 55 | } 56 | 57 | // Initialize implements sources.SourceConfig. 58 | func (c Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 59 | session, err := initCassandraSession(ctx, tracer, c) 60 | if err != nil { 61 | return nil, fmt.Errorf("unable to create session: %v", err) 62 | } 63 | s := &Source{ 64 | Name: c.Name, 65 | Kind: SourceKind, 66 | Session: session, 67 | } 68 | return s, nil 69 | } 70 | 71 | // SourceConfigKind implements sources.SourceConfig. 72 | func (c Config) SourceConfigKind() string { 73 | return SourceKind 74 | } 75 | 76 | var _ sources.SourceConfig = Config{} 77 | 78 | type Source struct { 79 | Name string `yaml:"name"` 80 | Kind string `yaml:"kind"` 81 | Session *gocql.Session 82 | } 83 | 84 | // CassandraSession implements cassandra.compatibleSource. 85 | func (s *Source) CassandraSession() *gocql.Session { 86 | return s.Session 87 | } 88 | 89 | // SourceKind implements sources.Source. 90 | func (s Source) SourceKind() string { 91 | return SourceKind 92 | } 93 | 94 | var _ sources.Source = &Source{} 95 | 96 | func initCassandraSession(ctx context.Context, tracer trace.Tracer, c Config) (*gocql.Session, error) { 97 | //nolint:all // Reassigned ctx 98 | ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, c.Name) 99 | defer span.End() 100 | 101 | // Validate authentication configuration 102 | if c.Password != "" && c.Username == "" { 103 | return nil, fmt.Errorf("invalid Cassandra configuration: password provided without a username") 104 | } 105 | 106 | cluster := gocql.NewCluster(c.Hosts...) 107 | cluster.ProtoVersion = c.ProtoVersion 108 | cluster.Keyspace = c.Keyspace 109 | 110 | // Configure authentication if username is provided 111 | if c.Username != "" { 112 | cluster.Authenticator = gocql.PasswordAuthenticator{ 113 | Username: c.Username, 114 | Password: c.Password, 115 | } 116 | } 117 | 118 | // Configure SSL options if any are specified 119 | if c.CAPath != "" || c.CertPath != "" || c.KeyPath != "" || c.EnableHostVerification { 120 | cluster.SslOpts = &gocql.SslOptions{ 121 | CaPath: c.CAPath, 122 | CertPath: c.CertPath, 123 | KeyPath: c.KeyPath, 124 | EnableHostVerification: c.EnableHostVerification, 125 | } 126 | } 127 | 128 | // Create session 129 | session, err := cluster.CreateSession() 130 | if err != nil { 131 | return nil, fmt.Errorf("failed to create Cassandra session: %w", err) 132 | } 133 | return session, nil 134 | } 135 | ``` -------------------------------------------------------------------------------- /internal/tools/common.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 tools 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "regexp" 22 | "text/template" 23 | ) 24 | 25 | var validName = regexp.MustCompile(`^[a-zA-Z0-9_-]*$`) 26 | 27 | func IsValidName(s string) bool { 28 | return validName.MatchString(s) 29 | } 30 | 31 | // ConvertAnySliceToTyped a []any to typed slice ([]string, []int, []float etc.) 32 | func ConvertAnySliceToTyped(s []any, itemType string) (any, error) { 33 | var typedSlice any 34 | switch itemType { 35 | case "string": 36 | tempSlice := make([]string, len(s)) 37 | for j, item := range s { 38 | s, ok := item.(string) 39 | if !ok { 40 | return nil, fmt.Errorf("expected item at index %d to be string, got %T", j, item) 41 | } 42 | tempSlice[j] = s 43 | } 44 | typedSlice = tempSlice 45 | case "integer": 46 | tempSlice := make([]int64, len(s)) 47 | for j, item := range s { 48 | i, ok := item.(int) 49 | if !ok { 50 | return nil, fmt.Errorf("expected item at index %d to be integer, got %T", j, item) 51 | } 52 | tempSlice[j] = int64(i) 53 | } 54 | typedSlice = tempSlice 55 | case "float": 56 | tempSlice := make([]float64, len(s)) 57 | for j, item := range s { 58 | f, ok := item.(float64) 59 | if !ok { 60 | return nil, fmt.Errorf("expected item at index %d to be float, got %T", j, item) 61 | } 62 | tempSlice[j] = f 63 | } 64 | typedSlice = tempSlice 65 | case "boolean": 66 | tempSlice := make([]bool, len(s)) 67 | for j, item := range s { 68 | b, ok := item.(bool) 69 | if !ok { 70 | return nil, fmt.Errorf("expected item at index %d to be boolean, got %T", j, item) 71 | } 72 | tempSlice[j] = b 73 | } 74 | typedSlice = tempSlice 75 | } 76 | return typedSlice, nil 77 | } 78 | 79 | // convertParamToJSON is a Go template helper function to convert a parameter to JSON formatted string. 80 | func convertParamToJSON(param any) (string, error) { 81 | jsonData, err := json.Marshal(param) 82 | if err != nil { 83 | return "", fmt.Errorf("failed to marshal param to JSON: %w", err) 84 | } 85 | return string(jsonData), nil 86 | } 87 | 88 | // PopulateTemplateWithJSON populate a Go template with a custom `json` array formatter 89 | func PopulateTemplateWithJSON(templateName, templateString string, data map[string]any) (string, error) { 90 | return PopulateTemplateWithFunc(templateName, templateString, data, template.FuncMap{ 91 | "json": convertParamToJSON, 92 | }) 93 | } 94 | 95 | // PopulateTemplate populate a Go template with no custom formatters 96 | func PopulateTemplate(templateName, templateString string, data map[string]any) (string, error) { 97 | return PopulateTemplateWithFunc(templateName, templateString, data, nil) 98 | } 99 | 100 | // PopulateTemplateWithFunc populate a Go template with provided functions 101 | func PopulateTemplateWithFunc(templateName, templateString string, data map[string]any, funcMap template.FuncMap) (string, error) { 102 | tmpl := template.New(templateName) 103 | if funcMap != nil { 104 | tmpl = tmpl.Funcs(funcMap) 105 | } 106 | 107 | parsedTmpl, err := tmpl.Parse(templateString) 108 | if err != nil { 109 | return "", fmt.Errorf("error parsing template '%s': %w", templateName, err) 110 | } 111 | 112 | var result bytes.Buffer 113 | if err := parsedTmpl.Execute(&result, data); err != nil { 114 | return "", fmt.Errorf("error executing template '%s': %w", templateName, err) 115 | } 116 | return result.String(), nil 117 | } 118 | 119 | // CheckDuplicateParameters verify there are no duplicate parameter names 120 | func CheckDuplicateParameters(ps Parameters) error { 121 | seenNames := make(map[string]bool) 122 | for _, p := range ps { 123 | pName := p.GetName() 124 | if _, exists := seenNames[pName]; exists { 125 | return fmt.Errorf("parameter name must be unique across all parameter fields. Duplicate parameter: %s", pName) 126 | } 127 | seenNames[pName] = true 128 | } 129 | return nil 130 | } 131 | ``` -------------------------------------------------------------------------------- /internal/tools/cloudmonitoring/cloudmonitoring_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 cloudmonitoring_test 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | yaml "github.com/goccy/go-yaml" 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/googleapis/genai-toolbox/internal/server" 24 | "github.com/googleapis/genai-toolbox/internal/testutils" 25 | "github.com/googleapis/genai-toolbox/internal/tools/cloudmonitoring" 26 | ) 27 | 28 | func TestParseFromYamlCloudMonitoring(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: cloud-monitoring-query-prometheus 44 | source: my-instance 45 | description: some description 46 | `, 47 | want: server.ToolConfigs{ 48 | "example_tool": cloudmonitoring.Config{ 49 | Name: "example_tool", 50 | Kind: "cloud-monitoring-query-prometheus", 51 | Source: "my-instance", 52 | Description: "some description", 53 | AuthRequired: []string{}, 54 | }, 55 | }, 56 | }, 57 | { 58 | desc: "advanced example", 59 | in: ` 60 | tools: 61 | example_tool: 62 | kind: cloud-monitoring-query-prometheus 63 | source: my-instance 64 | description: some description 65 | authRequired: 66 | - my-google-auth-service 67 | - other-auth-service 68 | `, 69 | want: server.ToolConfigs{ 70 | "example_tool": cloudmonitoring.Config{ 71 | Name: "example_tool", 72 | Kind: "cloud-monitoring-query-prometheus", 73 | Source: "my-instance", 74 | Description: "some description", 75 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 76 | }, 77 | }, 78 | }, 79 | } 80 | for _, tc := range tcs { 81 | t.Run(tc.desc, func(t *testing.T) { 82 | got := struct { 83 | Tools server.ToolConfigs `yaml:"tools"` 84 | }{} 85 | // Parse contents 86 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 87 | if err != nil { 88 | t.Fatalf("unable to unmarshal: %s", err) 89 | } 90 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 91 | t.Fatalf("incorrect parse: diff %v", diff) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestFailParseFromYamlCloudMonitoring(t *testing.T) { 98 | ctx, err := testutils.ContextWithNewLogger() 99 | if err != nil { 100 | t.Fatalf("unexpected error: %s", err) 101 | } 102 | tcs := []struct { 103 | desc string 104 | in string 105 | err string 106 | }{ 107 | { 108 | desc: "Invalid kind", 109 | in: ` 110 | tools: 111 | example_tool: 112 | kind: invalid-kind 113 | source: my-instance 114 | description: some description 115 | `, 116 | err: `unknown tool kind: "invalid-kind"`, 117 | }, 118 | { 119 | desc: "missing source", 120 | in: ` 121 | tools: 122 | example_tool: 123 | kind: cloud-monitoring-query-prometheus 124 | description: some description 125 | `, 126 | err: `Key: 'Config.Source' Error:Field validation for 'Source' failed on the 'required' tag`, 127 | }, 128 | { 129 | desc: "missing description", 130 | in: ` 131 | tools: 132 | example_tool: 133 | kind: cloud-monitoring-query-prometheus 134 | source: my-instance 135 | `, 136 | err: `Key: 'Config.Description' Error:Field validation for 'Description' failed on the 'required' tag`, 137 | }, 138 | } 139 | for _, tc := range tcs { 140 | t.Run(tc.desc, func(t *testing.T) { 141 | got := struct { 142 | Tools server.ToolConfigs `yaml:"tools"` 143 | }{} 144 | // Parse contents 145 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 146 | if err == nil { 147 | t.Fatalf("expect parsing to fail") 148 | } 149 | errStr := err.Error() 150 | if !strings.Contains(errStr, tc.err) { 151 | t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) 152 | } 153 | }) 154 | } 155 | } 156 | ``` -------------------------------------------------------------------------------- /internal/sources/cloudmonitoring/cloud_monitoring.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cloudmonitoring 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "net/http" 20 | 21 | "github.com/goccy/go-yaml" 22 | "github.com/googleapis/genai-toolbox/internal/sources" 23 | "github.com/googleapis/genai-toolbox/internal/util" 24 | "go.opentelemetry.io/otel/trace" 25 | "golang.org/x/oauth2" 26 | "golang.org/x/oauth2/google" 27 | monitoring "google.golang.org/api/monitoring/v3" 28 | ) 29 | 30 | const SourceKind string = "cloud-monitoring" 31 | 32 | type userAgentRoundTripper struct { 33 | userAgent string 34 | next http.RoundTripper 35 | } 36 | 37 | func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 38 | newReq := *req 39 | newReq.Header = make(http.Header) 40 | for k, v := range req.Header { 41 | newReq.Header[k] = v 42 | } 43 | ua := newReq.Header.Get("User-Agent") 44 | if ua == "" { 45 | newReq.Header.Set("User-Agent", rt.userAgent) 46 | } else { 47 | newReq.Header.Set("User-Agent", ua+" "+rt.userAgent) 48 | } 49 | return rt.next.RoundTrip(&newReq) 50 | } 51 | 52 | // validate interface 53 | var _ sources.SourceConfig = Config{} 54 | 55 | func init() { 56 | if !sources.Register(SourceKind, newConfig) { 57 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 58 | } 59 | } 60 | 61 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 62 | actual := Config{Name: name} 63 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 64 | return nil, err 65 | } 66 | return actual, nil 67 | } 68 | 69 | type Config struct { 70 | Name string `yaml:"name" validate:"required"` 71 | Kind string `yaml:"kind" validate:"required"` 72 | UseClientOAuth bool `yaml:"useClientOAuth"` 73 | } 74 | 75 | func (r Config) SourceConfigKind() string { 76 | return SourceKind 77 | } 78 | 79 | // Initialize initializes a Cloud Monitoring Source instance. 80 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 81 | ua, err := util.UserAgentFromContext(ctx) 82 | if err != nil { 83 | return nil, fmt.Errorf("error in User Agent retrieval: %s", err) 84 | } 85 | 86 | var client *http.Client 87 | if r.UseClientOAuth { 88 | client = &http.Client{ 89 | Transport: &userAgentRoundTripper{ 90 | userAgent: ua, 91 | next: http.DefaultTransport, 92 | }, 93 | } 94 | } else { 95 | // Use Application Default Credentials 96 | creds, err := google.FindDefaultCredentials(ctx, monitoring.MonitoringScope) 97 | if err != nil { 98 | return nil, fmt.Errorf("failed to find default credentials: %w", err) 99 | } 100 | baseClient := oauth2.NewClient(ctx, creds.TokenSource) 101 | baseClient.Transport = &userAgentRoundTripper{ 102 | userAgent: ua, 103 | next: baseClient.Transport, 104 | } 105 | client = baseClient 106 | } 107 | 108 | s := &Source{ 109 | Name: r.Name, 110 | Kind: SourceKind, 111 | BaseURL: "https://monitoring.googleapis.com", 112 | Client: client, 113 | UserAgent: ua, 114 | UseClientOAuth: r.UseClientOAuth, 115 | } 116 | return s, nil 117 | } 118 | 119 | var _ sources.Source = &Source{} 120 | 121 | type Source struct { 122 | Name string `yaml:"name"` 123 | Kind string `yaml:"kind"` 124 | BaseURL string `yaml:"baseUrl"` 125 | Client *http.Client 126 | UserAgent string 127 | UseClientOAuth bool 128 | } 129 | 130 | func (s *Source) SourceKind() string { 131 | return SourceKind 132 | } 133 | 134 | func (s *Source) GetClient(ctx context.Context, accessToken string) (*http.Client, error) { 135 | if s.UseClientOAuth { 136 | if accessToken == "" { 137 | return nil, fmt.Errorf("client-side OAuth is enabled but no access token was provided") 138 | } 139 | token := &oauth2.Token{AccessToken: accessToken} 140 | return oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)), nil 141 | } 142 | return s.Client, nil 143 | } 144 | 145 | func (s *Source) UseClientAuthorization() bool { 146 | return s.UseClientOAuth 147 | } 148 | ``` -------------------------------------------------------------------------------- /docs/en/resources/tools/mongodb/mongodb-find.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "mongodb-find" 3 | type: docs 4 | weight: 1 5 | description: > 6 | A "mongodb-find" tool finds and retrieves documents from a MongoDB collection. 7 | aliases: 8 | - /resources/tools/mongodb-find 9 | --- 10 | 11 | ## About 12 | 13 | A `mongodb-find` tool is used to query a MongoDB collection and retrieve 14 | documents that match a specified filter. It's a flexible tool that allows you to 15 | shape the output by selecting specific fields (**projection**), ordering the 16 | results (**sorting**), and restricting the number of documents returned 17 | (**limiting**). 18 | 19 | The tool returns a JSON array of the documents found. 20 | 21 | This tool is compatible with the following source kind: 22 | 23 | * [`mongodb`](../../sources/mongodb.md) 24 | 25 | ## Example 26 | 27 | Here's an example that finds up to 10 users from the `customers` collection who 28 | live in a specific city. The results are sorted by their last name, and only 29 | their first name, last name, and email are returned. 30 | 31 | ```yaml 32 | tools: 33 | find_local_customers: 34 | kind: mongodb-find 35 | source: my-mongo-source 36 | description: Finds customers by city, sorted by last name. 37 | database: crm 38 | collection: customers 39 | limit: 10 40 | filterPayload: | 41 | { "address.city": {{json .city}} } 42 | filterParams: 43 | - name: city 44 | type: string 45 | description: The city to search for customers in. 46 | projectPayload: | 47 | { 48 | "first_name": 1, 49 | "last_name": 1, 50 | "email": 1, 51 | "_id": 0 52 | } 53 | sortPayload: | 54 | { "last_name": {{json .sort_order}} } 55 | sortParams: 56 | - name: sort_order 57 | type: integer 58 | description: The sort order (1 for ascending, -1 for descending). 59 | ``` 60 | 61 | ## Reference 62 | 63 | | **field** | **type** | **required** | **description** | 64 | |:---------------|:---------|:-------------|:----------------------------------------------------------------------------------------------------------------------------| 65 | | kind | string | true | Must be `mongodb-find`. | 66 | | source | string | true | The name of the `mongodb` source to use. | 67 | | description | string | true | A description of the tool that is passed to the LLM. | 68 | | database | string | true | The name of the MongoDB database to query. | 69 | | collection | string | true | The name of the MongoDB collection to query. | 70 | | filterPayload | string | true | The MongoDB query filter document to select which documents to return. Uses `{{json .param_name}}` for templating. | 71 | | filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. | 72 | | projectPayload | string | false | An optional MongoDB projection document to specify which fields to include (1) or exclude (0) in the results. | 73 | | projectParams | list | false | A list of parameter objects for the `projectPayload`. | 74 | | sortPayload | string | false | An optional MongoDB sort document to define the order of the returned documents. Use 1 for ascending and -1 for descending. | 75 | | sortParams | list | false | A list of parameter objects for the `sortPayload`. | 76 | | limit | integer | false | An optional integer specifying the maximum number of documents to return. | 77 | ``` -------------------------------------------------------------------------------- /internal/sources/http/http_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package http_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/sources" 24 | "github.com/googleapis/genai-toolbox/internal/sources/http" 25 | "github.com/googleapis/genai-toolbox/internal/testutils" 26 | ) 27 | 28 | func TestParseFromYamlHttp(t *testing.T) { 29 | tcs := []struct { 30 | desc string 31 | in string 32 | want server.SourceConfigs 33 | }{ 34 | { 35 | desc: "basic example", 36 | in: ` 37 | sources: 38 | my-http-instance: 39 | kind: http 40 | baseUrl: http://test_server/ 41 | `, 42 | want: map[string]sources.SourceConfig{ 43 | "my-http-instance": http.Config{ 44 | Name: "my-http-instance", 45 | Kind: http.SourceKind, 46 | BaseURL: "http://test_server/", 47 | Timeout: "30s", 48 | DisableSslVerification: false, 49 | }, 50 | }, 51 | }, 52 | { 53 | desc: "advanced example", 54 | in: ` 55 | sources: 56 | my-http-instance: 57 | kind: http 58 | baseUrl: http://test_server/ 59 | timeout: 10s 60 | headers: 61 | Authorization: test_header 62 | Custom-Header: custom 63 | queryParams: 64 | api-key: test_api_key 65 | param: param-value 66 | disableSslVerification: true 67 | `, 68 | want: map[string]sources.SourceConfig{ 69 | "my-http-instance": http.Config{ 70 | Name: "my-http-instance", 71 | Kind: http.SourceKind, 72 | BaseURL: "http://test_server/", 73 | Timeout: "10s", 74 | DefaultHeaders: map[string]string{"Authorization": "test_header", "Custom-Header": "custom"}, 75 | QueryParams: map[string]string{"api-key": "test_api_key", "param": "param-value"}, 76 | DisableSslVerification: true, 77 | }, 78 | }, 79 | }, 80 | } 81 | for _, tc := range tcs { 82 | t.Run(tc.desc, func(t *testing.T) { 83 | got := struct { 84 | Sources server.SourceConfigs `yaml:"sources"` 85 | }{} 86 | // Parse contents 87 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 88 | if err != nil { 89 | t.Fatalf("unable to unmarshal: %s", err) 90 | } 91 | if !cmp.Equal(tc.want, got.Sources) { 92 | t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources) 93 | } 94 | }) 95 | } 96 | } 97 | 98 | func TestFailParseFromYaml(t *testing.T) { 99 | tcs := []struct { 100 | desc string 101 | in string 102 | err string 103 | }{ 104 | { 105 | desc: "extra field", 106 | in: ` 107 | sources: 108 | my-http-instance: 109 | kind: http 110 | baseUrl: http://test_server/ 111 | timeout: 10s 112 | headers: 113 | Authorization: test_header 114 | queryParams: 115 | api-key: test_api_key 116 | project: test-project 117 | `, 118 | err: "unable to parse source \"my-http-instance\" as \"http\": [5:1] unknown field \"project\"\n 2 | headers:\n 3 | Authorization: test_header\n 4 | kind: http\n> 5 | project: test-project\n ^\n 6 | queryParams:\n 7 | api-key: test_api_key\n 8 | timeout: 10s", 119 | }, 120 | { 121 | desc: "missing required field", 122 | in: ` 123 | sources: 124 | my-http-instance: 125 | baseUrl: http://test_server/ 126 | `, 127 | err: "missing 'kind' field for source \"my-http-instance\"", 128 | }, 129 | } 130 | for _, tc := range tcs { 131 | t.Run(tc.desc, func(t *testing.T) { 132 | got := struct { 133 | Sources server.SourceConfigs `yaml:"sources"` 134 | }{} 135 | // Parse contents 136 | err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got) 137 | if err == nil { 138 | t.Fatalf("expect parsing to fail") 139 | } 140 | errStr := err.Error() 141 | if errStr != tc.err { 142 | t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) 143 | } 144 | }) 145 | } 146 | } 147 | ``` -------------------------------------------------------------------------------- /internal/tools/alloydbainl/alloydbainl_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 alloydbainl_test 16 | 17 | import ( 18 | "testing" 19 | 20 | yaml "github.com/goccy/go-yaml" 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/googleapis/genai-toolbox/internal/server" 23 | "github.com/googleapis/genai-toolbox/internal/testutils" 24 | "github.com/googleapis/genai-toolbox/internal/tools" 25 | "github.com/googleapis/genai-toolbox/internal/tools/alloydbainl" 26 | ) 27 | 28 | func TestParseFromYamlAlloyDBNLA(t *testing.T) { 29 | ctx, err := testutils.ContextWithNewLogger() 30 | if err != nil { 31 | t.Fatalf("unexpected error: %s", err) 32 | } 33 | tcs := []struct { 34 | desc string 35 | in string 36 | want server.ToolConfigs 37 | }{ 38 | { 39 | desc: "basic example", 40 | in: ` 41 | tools: 42 | example_tool: 43 | kind: alloydb-ai-nl 44 | source: my-alloydb-instance 45 | description: AlloyDB natural language query tool 46 | nlConfig: 'my_nl_config' 47 | authRequired: 48 | - my-google-auth-service 49 | nlConfigParameters: 50 | - name: user_id 51 | type: string 52 | description: user_id to use 53 | authServices: 54 | - name: my-google-auth-service 55 | field: sub 56 | `, 57 | want: server.ToolConfigs{ 58 | "example_tool": alloydbainl.Config{ 59 | Name: "example_tool", 60 | Kind: "alloydb-ai-nl", 61 | Source: "my-alloydb-instance", 62 | Description: "AlloyDB natural language query tool", 63 | NLConfig: "my_nl_config", 64 | AuthRequired: []string{"my-google-auth-service"}, 65 | NLConfigParameters: []tools.Parameter{ 66 | tools.NewStringParameterWithAuth("user_id", "user_id to use", 67 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "sub"}}), 68 | }, 69 | }, 70 | }, 71 | }, 72 | { 73 | desc: "with multiple parameters", 74 | in: ` 75 | tools: 76 | complex_tool: 77 | kind: alloydb-ai-nl 78 | source: my-alloydb-instance 79 | description: AlloyDB natural language query tool with multiple parameters 80 | nlConfig: 'complex_nl_config' 81 | authRequired: 82 | - my-google-auth-service 83 | - other-auth-service 84 | nlConfigParameters: 85 | - name: user_id 86 | type: string 87 | description: user_id to use 88 | authServices: 89 | - name: my-google-auth-service 90 | field: sub 91 | - name: user_email 92 | type: string 93 | description: user_email to use 94 | authServices: 95 | - name: my-google-auth-service 96 | field: user_email 97 | `, 98 | want: server.ToolConfigs{ 99 | "complex_tool": alloydbainl.Config{ 100 | Name: "complex_tool", 101 | Kind: "alloydb-ai-nl", 102 | Source: "my-alloydb-instance", 103 | Description: "AlloyDB natural language query tool with multiple parameters", 104 | NLConfig: "complex_nl_config", 105 | AuthRequired: []string{"my-google-auth-service", "other-auth-service"}, 106 | NLConfigParameters: []tools.Parameter{ 107 | tools.NewStringParameterWithAuth("user_id", "user_id to use", 108 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "sub"}}), 109 | tools.NewStringParameterWithAuth("user_email", "user_email to use", 110 | []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_email"}}), 111 | }, 112 | }, 113 | }, 114 | }, 115 | } 116 | for _, tc := range tcs { 117 | t.Run(tc.desc, func(t *testing.T) { 118 | got := struct { 119 | Tools server.ToolConfigs `yaml:"tools"` 120 | }{} 121 | // Parse contents 122 | err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got) 123 | if err != nil { 124 | t.Fatalf("unable to unmarshal: %s", err) 125 | } 126 | if diff := cmp.Diff(tc.want, got.Tools); diff != "" { 127 | t.Fatalf("incorrect parse: diff %v", diff) 128 | } 129 | }) 130 | } 131 | } 132 | ``` -------------------------------------------------------------------------------- /tests/redis/redis_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 redis 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "regexp" 22 | "testing" 23 | "time" 24 | 25 | "github.com/googleapis/genai-toolbox/internal/testutils" 26 | "github.com/googleapis/genai-toolbox/tests" 27 | "github.com/redis/go-redis/v9" 28 | ) 29 | 30 | var ( 31 | RedisSourceKind = "redis" 32 | RedisToolKind = "redis" 33 | RedisAddress = os.Getenv("REDIS_ADDRESS") 34 | RedisPass = os.Getenv("REDIS_PASS") 35 | ) 36 | 37 | func getRedisVars(t *testing.T) map[string]any { 38 | switch "" { 39 | case RedisAddress: 40 | t.Fatal("'REDIS_ADDRESS' not set") 41 | case RedisPass: 42 | t.Fatal("'REDIS_PASS' not set") 43 | } 44 | return map[string]any{ 45 | "kind": RedisSourceKind, 46 | "address": []string{RedisAddress}, 47 | "password": RedisPass, 48 | } 49 | } 50 | 51 | func initRedisClient(ctx context.Context, address, pass string) (*redis.Client, error) { 52 | // Create a new Redis client 53 | standaloneClient := redis.NewClient(&redis.Options{ 54 | Addr: address, 55 | PoolSize: 10, 56 | ConnMaxIdleTime: 60 * time.Second, 57 | MinIdleConns: 1, 58 | Password: pass, 59 | }) 60 | _, err := standaloneClient.Ping(ctx).Result() 61 | if err != nil { 62 | return nil, fmt.Errorf("unable to connect to redis: %s", err) 63 | } 64 | return standaloneClient, nil 65 | } 66 | 67 | func TestRedisToolEndpoints(t *testing.T) { 68 | sourceConfig := getRedisVars(t) 69 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 70 | defer cancel() 71 | 72 | var args []string 73 | 74 | client, err := initRedisClient(ctx, RedisAddress, RedisPass) 75 | if err != nil { 76 | t.Fatalf("unable to create Redis connection: %s", err) 77 | } 78 | 79 | // set up data for param tool 80 | teardownDB := setupRedisDB(t, ctx, client) 81 | defer teardownDB(t) 82 | 83 | // Write config into a file and pass it to command 84 | toolsFile := tests.GetRedisValkeyToolsConfig(sourceConfig, RedisToolKind) 85 | 86 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 87 | if err != nil { 88 | t.Fatalf("command initialization returned an error: %s", err) 89 | } 90 | defer cleanup() 91 | 92 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 93 | defer cancel() 94 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 95 | if err != nil { 96 | t.Logf("toolbox command logs: \n%s", out) 97 | t.Fatalf("toolbox didn't start successfully: %s", err) 98 | } 99 | 100 | // Get configs for tests 101 | select1Want, mcpMyFailToolWant, invokeParamWant, invokeIdNullWant, nullWant, mcpSelect1Want, mcpInvokeParamWant := tests.GetRedisValkeyWants() 102 | 103 | // Run tests 104 | tests.RunToolGetTest(t) 105 | tests.RunToolInvokeTest(t, select1Want, 106 | tests.WithMyToolId3NameAliceWant(invokeParamWant), 107 | tests.WithMyArrayToolWant(invokeParamWant), 108 | tests.WithMyToolById4Want(invokeIdNullWant), 109 | tests.WithNullWant(nullWant), 110 | ) 111 | tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want, 112 | tests.WithMcpMyToolId3NameAliceWant(mcpInvokeParamWant), 113 | ) 114 | } 115 | 116 | func setupRedisDB(t *testing.T, ctx context.Context, client *redis.Client) func(*testing.T) { 117 | keys := []string{"row1", "row2", "row3", "row4", "null"} 118 | commands := [][]any{ 119 | {"HSET", keys[0], "id", 1, "name", "Alice"}, 120 | {"HSET", keys[1], "id", 2, "name", "Jane"}, 121 | {"HSET", keys[2], "id", 3, "name", "Sid"}, 122 | {"HSET", keys[3], "id", 4, "name", nil}, 123 | {"SET", keys[4], "null"}, 124 | {"HSET", tests.ServiceAccountEmail, "name", "Alice"}, 125 | } 126 | for _, c := range commands { 127 | resp := client.Do(ctx, c...) 128 | if err := resp.Err(); err != nil { 129 | t.Fatalf("unable to insert test data: %s", err) 130 | } 131 | } 132 | 133 | return func(t *testing.T) { 134 | // tear down test 135 | _, err := client.Del(ctx, keys...).Result() 136 | if err != nil { 137 | t.Errorf("Teardown failed: %s", err) 138 | } 139 | } 140 | 141 | } 142 | ``` -------------------------------------------------------------------------------- /internal/server/mcp/jsonrpc/jsonrpc.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 jsonrpc 16 | 17 | // JSONRPC_VERSION is the version of JSON-RPC used by MCP. 18 | const JSONRPC_VERSION = "2.0" 19 | 20 | // Standard JSON-RPC error codes 21 | const ( 22 | PARSE_ERROR = -32700 23 | INVALID_REQUEST = -32600 24 | METHOD_NOT_FOUND = -32601 25 | INVALID_PARAMS = -32602 26 | INTERNAL_ERROR = -32603 27 | ) 28 | 29 | // ProgressToken is used to associate progress notifications with the original request. 30 | type ProgressToken interface{} 31 | 32 | // RequestId is a uniquely identifying ID for a request in JSON-RPC. 33 | // It can be any JSON-serializable value, typically a number or string. 34 | type RequestId interface{} 35 | 36 | // Request represents a bidirectional message with method and parameters expecting a response. 37 | type Request struct { 38 | Method string `json:"method"` 39 | Params struct { 40 | Meta struct { 41 | // If specified, the caller is requesting out-of-band progress 42 | // notifications for this request (as represented by 43 | // notifications/progress). The value of this parameter is an 44 | // opaque token that will be attached to any subsequent 45 | // notifications. The receiver is not obligated to provide these 46 | // notifications. 47 | ProgressToken ProgressToken `json:"progressToken,omitempty"` 48 | } `json:"_meta,omitempty"` 49 | } `json:"params,omitempty"` 50 | } 51 | 52 | // JSONRPCRequest represents a request that expects a response. 53 | type JSONRPCRequest struct { 54 | Jsonrpc string `json:"jsonrpc"` 55 | Id RequestId `json:"id"` 56 | Request 57 | Params any `json:"params,omitempty"` 58 | } 59 | 60 | // Notification is a one-way message requiring no response. 61 | type Notification struct { 62 | Method string `json:"method"` 63 | Params struct { 64 | Meta map[string]interface{} `json:"_meta,omitempty"` 65 | } `json:"params,omitempty"` 66 | } 67 | 68 | // JSONRPCNotification represents a notification which does not expect a response. 69 | type JSONRPCNotification struct { 70 | Jsonrpc string `json:"jsonrpc"` 71 | Notification 72 | } 73 | 74 | // Result represents a response for the request query. 75 | type Result struct { 76 | // This result property is reserved by the protocol to allow clients and 77 | // servers to attach additional metadata to their responses. 78 | Meta map[string]interface{} `json:"_meta,omitempty"` 79 | } 80 | 81 | // JSONRPCResponse represents a successful (non-error) response to a request. 82 | type JSONRPCResponse struct { 83 | Jsonrpc string `json:"jsonrpc"` 84 | Id RequestId `json:"id"` 85 | Result interface{} `json:"result"` 86 | } 87 | 88 | // Error represents the error content. 89 | type Error struct { 90 | // The error type that occurred. 91 | Code int `json:"code"` 92 | // A short description of the error. The message SHOULD be limited 93 | // to a concise single sentence. 94 | Message string `json:"message"` 95 | // Additional information about the error. The value of this member 96 | // is defined by the sender (e.g. detailed error information, nested errors etc.). 97 | Data interface{} `json:"data,omitempty"` 98 | } 99 | 100 | // JSONRPCError represents a non-successful (error) response to a request. 101 | type JSONRPCError struct { 102 | Jsonrpc string `json:"jsonrpc"` 103 | Id RequestId `json:"id"` 104 | Error Error `json:"error"` 105 | } 106 | 107 | // Generic baseMessage could either be a JSONRPCNotification or JSONRPCRequest 108 | type BaseMessage struct { 109 | Jsonrpc string `json:"jsonrpc"` 110 | Method string `json:"method"` 111 | Id RequestId `json:"id,omitempty"` 112 | } 113 | 114 | // NewError is the standard JSONRPC response sent back when an error has been encountered. 115 | func NewError(id RequestId, code int, message string, data any) JSONRPCError { 116 | return JSONRPCError{ 117 | Jsonrpc: JSONRPC_VERSION, 118 | Id: id, 119 | Error: Error{ 120 | Code: code, 121 | Message: message, 122 | Data: data, 123 | }, 124 | } 125 | } 126 | ``` -------------------------------------------------------------------------------- /internal/sources/mysql/mysql.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 mysql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "net/url" 22 | "time" 23 | 24 | _ "github.com/go-sql-driver/mysql" 25 | "github.com/goccy/go-yaml" 26 | "github.com/googleapis/genai-toolbox/internal/sources" 27 | "github.com/googleapis/genai-toolbox/internal/util" 28 | "go.opentelemetry.io/otel/trace" 29 | ) 30 | 31 | const SourceKind string = "mysql" 32 | 33 | // validate interface 34 | var _ sources.SourceConfig = Config{} 35 | 36 | func init() { 37 | if !sources.Register(SourceKind, newConfig) { 38 | panic(fmt.Sprintf("source kind %q already registered", SourceKind)) 39 | } 40 | } 41 | 42 | func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) { 43 | actual := Config{Name: name} 44 | if err := decoder.DecodeContext(ctx, &actual); err != nil { 45 | return nil, err 46 | } 47 | return actual, nil 48 | } 49 | 50 | type Config struct { 51 | Name string `yaml:"name" validate:"required"` 52 | Kind string `yaml:"kind" validate:"required"` 53 | Host string `yaml:"host" validate:"required"` 54 | Port string `yaml:"port" validate:"required"` 55 | User string `yaml:"user" validate:"required"` 56 | Password string `yaml:"password" validate:"required"` 57 | Database string `yaml:"database" validate:"required"` 58 | QueryTimeout string `yaml:"queryTimeout"` 59 | QueryParams map[string]string `yaml:"queryParams"` 60 | } 61 | 62 | func (r Config) SourceConfigKind() string { 63 | return SourceKind 64 | } 65 | 66 | func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { 67 | pool, err := initMySQLConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryTimeout, r.QueryParams) 68 | if err != nil { 69 | return nil, fmt.Errorf("unable to create pool: %w", err) 70 | } 71 | 72 | err = pool.PingContext(ctx) 73 | if err != nil { 74 | return nil, fmt.Errorf("unable to connect successfully: %w", err) 75 | } 76 | 77 | s := &Source{ 78 | Name: r.Name, 79 | Kind: SourceKind, 80 | Pool: pool, 81 | } 82 | return s, nil 83 | } 84 | 85 | var _ sources.Source = &Source{} 86 | 87 | type Source struct { 88 | Name string `yaml:"name"` 89 | Kind string `yaml:"kind"` 90 | Pool *sql.DB 91 | } 92 | 93 | func (s *Source) SourceKind() string { 94 | return SourceKind 95 | } 96 | 97 | func (s *Source) MySQLPool() *sql.DB { 98 | return s.Pool 99 | } 100 | 101 | func initMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname, queryTimeout string, queryParams map[string]string) (*sql.DB, error) { 102 | //nolint:all // Reassigned ctx 103 | ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name) 104 | defer span.End() 105 | 106 | // Build query parameters via url.Values for deterministic order and proper escaping. 107 | values := url.Values{} 108 | 109 | // Derive readTimeout from queryTimeout when provided. 110 | if queryTimeout != "" { 111 | timeout, err := time.ParseDuration(queryTimeout) 112 | if err != nil { 113 | return nil, fmt.Errorf("invalid queryTimeout %q: %w", queryTimeout, err) 114 | } 115 | values.Set("readTimeout", timeout.String()) 116 | } 117 | 118 | // Custom user parameters 119 | for k, v := range queryParams { 120 | if v == "" { 121 | continue // skip empty values 122 | } 123 | values.Set(k, v) 124 | } 125 | 126 | userAgent, err := util.UserAgentFromContext(ctx) 127 | if err != nil { 128 | return nil, err 129 | } 130 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&connectionAttributes=program_name:%s", user, pass, host, port, dbname, url.QueryEscape(userAgent)) 131 | if enc := values.Encode(); enc != "" { 132 | dsn += "&" + enc 133 | } 134 | 135 | // Interact with the driver directly as you normally would 136 | pool, err := sql.Open("mysql", dsn) 137 | if err != nil { 138 | return nil, fmt.Errorf("sql.Open: %w", err) 139 | } 140 | return pool, nil 141 | } 142 | ``` -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- ```yaml 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 | name: tests 16 | on: 17 | push: 18 | branches: 19 | - "main" 20 | pull_request: 21 | pull_request_target: 22 | types: [labeled] 23 | 24 | # Declare default permissions as read only. 25 | permissions: read-all 26 | 27 | jobs: 28 | integration: 29 | # run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label) 30 | if: "${{ (github.event.action != 'labeled' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) || github.event.label.name == 'tests: run' }}" 31 | name: unit tests 32 | runs-on: ${{ matrix.os }} 33 | strategy: 34 | matrix: 35 | os: [macos-latest, windows-latest, ubuntu-latest] 36 | fail-fast: false 37 | permissions: 38 | contents: "read" 39 | issues: "write" 40 | pull-requests: "write" 41 | steps: 42 | - name: Remove PR label 43 | if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" 44 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 45 | with: 46 | github-token: ${{ secrets.GITHUB_TOKEN }} 47 | script: | 48 | try { 49 | await github.rest.issues.removeLabel({ 50 | name: 'tests: run', 51 | owner: context.repo.owner, 52 | repo: context.repo.repo, 53 | issue_number: context.payload.pull_request.number 54 | }); 55 | } catch (e) { 56 | console.log('Failed to remove label. Another job may have already removed it!'); 57 | } 58 | 59 | - name: Setup Go 60 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 61 | with: 62 | go-version: "1.24" 63 | 64 | - name: Checkout code 65 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 66 | with: 67 | ref: ${{ github.event.pull_request.head.sha }} 68 | repository: ${{ github.event.pull_request.head.repo.full_name }} 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | - name: Install dependencies 72 | run: go get . 73 | 74 | - name: Build 75 | run: go build -v ./... 76 | 77 | - name: Run tests with coverage 78 | if: ${{ runner.os == 'Linux' }} 79 | env: 80 | GOTOOLCHAIN: go1.25.0+auto 81 | run: | 82 | source_dir="./internal/sources/*" 83 | tool_dir="./internal/tools/*" 84 | auth_dir="./internal/auth/*" 85 | int_test_dir="./tests/*" 86 | included_packages=$(go list ./... | grep -v -e "$source_dir" -e "$tool_dir" -e "$auth_dir" -e "$int_test_dir") 87 | go test -race -cover -coverprofile=coverage.out -v $included_packages 88 | go test -race -v ./internal/sources/... ./internal/tools/... ./internal/auth/... 89 | 90 | - name: Run tests without coverage 91 | if: ${{ runner.os != 'Linux' }} 92 | run: | 93 | go test -race -v ./internal/... ./cmd/... 94 | 95 | - name: Check coverage 96 | if: ${{ runner.os == 'Linux' }} 97 | run: | 98 | FILE_TO_EXCLUDE="github.com/googleapis/genai-toolbox/internal/server/config.go" 99 | ESCAPED_PATH=$(echo "$FILE_TO_EXCLUDE" | sed 's/\//\\\//g; s/\./\\\./g') 100 | sed -i "/^${ESCAPED_PATH}:/d" coverage.out 101 | total_coverage=$(go tool cover -func=coverage.out | grep "total:" | awk '{print $3}') 102 | echo "Total coverage: $total_coverage" 103 | coverage_numeric=$(echo "$total_coverage" | sed 's/%//') 104 | if (( $(echo "$coverage_numeric < 40" | bc -l) )); then 105 | echo "Coverage failure: total coverage($total_coverage) is below 40%." 106 | exit 1 107 | fi 108 | ``` -------------------------------------------------------------------------------- /tests/dataform/dataform_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 dataformcompilelocal 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/http" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "regexp" 25 | "strings" 26 | "testing" 27 | "time" 28 | 29 | "github.com/googleapis/genai-toolbox/internal/testutils" 30 | "github.com/googleapis/genai-toolbox/tests" 31 | ) 32 | 33 | // setupTestProject creates a minimal dataform project using the 'dataform init' CLI. 34 | // It returns the path to the directory and a cleanup function. 35 | func setupTestProject(t *testing.T) (string, func()) { 36 | tmpDir, err := os.MkdirTemp("", "dataform-project-*") 37 | if err != nil { 38 | t.Fatalf("Failed to create temp dir: %v", err) 39 | } 40 | cleanup := func() { 41 | os.RemoveAll(tmpDir) 42 | } 43 | 44 | cmd := exec.Command("dataform", "init", tmpDir, "test-project-id", "US") 45 | if output, err := cmd.CombinedOutput(); err != nil { 46 | cleanup() 47 | t.Fatalf("Failed to run 'dataform init': %v\nOutput: %s", err, string(output)) 48 | } 49 | 50 | definitionsDir := filepath.Join(tmpDir, "definitions") 51 | exampleSQLX := `config { type: "table" } SELECT 1 AS test_col` 52 | err = os.WriteFile(filepath.Join(definitionsDir, "example.sqlx"), []byte(exampleSQLX), 0644) 53 | if err != nil { 54 | cleanup() 55 | t.Fatalf("Failed to write example.sqlx: %v", err) 56 | } 57 | 58 | return tmpDir, cleanup 59 | } 60 | 61 | func TestDataformCompileTool(t *testing.T) { 62 | if _, err := exec.LookPath("dataform"); err != nil { 63 | t.Skip("dataform CLI not found in $PATH, skipping integration test") 64 | } 65 | 66 | projectDir, cleanupProject := setupTestProject(t) 67 | defer cleanupProject() 68 | 69 | toolsFile := map[string]any{ 70 | "tools": map[string]any{ 71 | "my-dataform-compiler": map[string]any{ 72 | "kind": "dataform-compile-local", 73 | "description": "Tool to compile dataform projects", 74 | }, 75 | }, 76 | } 77 | 78 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) 79 | defer cancel() 80 | 81 | cmd, cleanupServer, err := tests.StartCmd(ctx, toolsFile) 82 | if err != nil { 83 | t.Fatalf("command initialization returned an error: %s", err) 84 | } 85 | defer cleanupServer() 86 | 87 | waitCtx, cancelWait := context.WithTimeout(ctx, 30*time.Second) 88 | defer cancelWait() 89 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 90 | if err != nil { 91 | t.Logf("toolbox command logs: \n%s", out) 92 | t.Fatalf("toolbox didn't start successfully: %s", err) 93 | } 94 | 95 | nonExistentDir := filepath.Join(os.TempDir(), "non-existent-dir") 96 | 97 | testCases := []struct { 98 | name string 99 | reqBody string 100 | wantStatus int 101 | wantBody string // Substring to check for in the response 102 | }{ 103 | { 104 | name: "success case", 105 | reqBody: fmt.Sprintf(`{"project_dir":"%s"}`, projectDir), 106 | wantStatus: http.StatusOK, 107 | wantBody: "test_col", 108 | }, 109 | { 110 | name: "missing parameter", 111 | reqBody: `{}`, 112 | wantStatus: http.StatusBadRequest, 113 | wantBody: `parameter \"project_dir\" is required`, 114 | }, 115 | { 116 | name: "non-existent directory", 117 | reqBody: fmt.Sprintf(`{"project_dir":"%s"}`, nonExistentDir), 118 | wantStatus: http.StatusBadRequest, 119 | wantBody: "error executing dataform compile", 120 | }, 121 | } 122 | 123 | api := "http://127.0.0.1:5000/api/tool/my-dataform-compiler/invoke" 124 | 125 | for _, tc := range testCases { 126 | t.Run(tc.name, func(t *testing.T) { 127 | resp, bodyBytes := tests.RunRequest(t, http.MethodPost, api, strings.NewReader(tc.reqBody), nil) 128 | 129 | if resp.StatusCode != tc.wantStatus { 130 | t.Fatalf("unexpected status: got %d, want %d. Body: %s", resp.StatusCode, tc.wantStatus, string(bodyBytes)) 131 | } 132 | 133 | if tc.wantBody != "" && !strings.Contains(string(bodyBytes), tc.wantBody) { 134 | t.Fatalf("expected body to contain %q, got: %s", tc.wantBody, string(bodyBytes)) 135 | } 136 | }) 137 | } 138 | } 139 | ``` -------------------------------------------------------------------------------- /docs/en/getting-started/quickstart/go/openAI/quickstart.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/googleapis/mcp-toolbox-sdk-go/core" 10 | openai "github.com/openai/openai-go" 11 | ) 12 | 13 | // ConvertToOpenAITool converts a ToolboxTool into the go-openai library's Tool format. 14 | func ConvertToOpenAITool(toolboxTool *core.ToolboxTool) openai.ChatCompletionToolParam { 15 | // Get the input schema 16 | jsonSchemaBytes, err := toolboxTool.InputSchema() 17 | if err != nil { 18 | return openai.ChatCompletionToolParam{} 19 | } 20 | 21 | // Unmarshal the JSON bytes into FunctionParameters 22 | var paramsSchema openai.FunctionParameters 23 | if err := json.Unmarshal(jsonSchemaBytes, ¶msSchema); err != nil { 24 | return openai.ChatCompletionToolParam{} 25 | } 26 | 27 | // Create and return the final tool parameter struct. 28 | return openai.ChatCompletionToolParam{ 29 | Function: openai.FunctionDefinitionParam{ 30 | Name: toolboxTool.Name(), 31 | Description: openai.String(toolboxTool.Description()), 32 | Parameters: paramsSchema, 33 | }, 34 | } 35 | } 36 | 37 | const systemPrompt = ` 38 | You're a helpful hotel assistant. You handle hotel searching, booking, and 39 | cancellations. When the user searches for a hotel, mention its name, id, 40 | location and price tier. Always mention hotel ids while performing any 41 | searches. This is very important for any operations. For any bookings or 42 | cancellations, please provide the appropriate confirmation. Be sure to 43 | update checkin or checkout dates if mentioned by the user. 44 | Don't ask for confirmations from the user. 45 | ` 46 | 47 | var queries = []string{ 48 | "Find hotels in Basel with Basel in its name.", 49 | "Can you book the hotel Hilton Basel for me?", 50 | "Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.", 51 | "My check in dates would be from April 10, 2024 to April 19, 2024.", 52 | } 53 | 54 | func main() { 55 | // Setup 56 | ctx := context.Background() 57 | toolboxURL := "http://localhost:5000" 58 | openAIClient := openai.NewClient() 59 | 60 | // Initialize the MCP Toolbox client. 61 | toolboxClient, err := core.NewToolboxClient(toolboxURL) 62 | if err != nil { 63 | log.Fatalf("Failed to create Toolbox client: %v", err) 64 | } 65 | 66 | // Load the tools using the MCP Toolbox SDK. 67 | tools, err := toolboxClient.LoadToolset("my-toolset", ctx) 68 | if err != nil { 69 | log.Fatalf("Failed to load tool : %v\nMake sure your Toolbox server is running and the tool is configured.", err) 70 | } 71 | 72 | openAITools := make([]openai.ChatCompletionToolParam, len(tools)) 73 | toolsMap := make(map[string]*core.ToolboxTool, len(tools)) 74 | 75 | for i, tool := range tools { 76 | // Convert the Toolbox tool into the openAI FunctionDeclaration format. 77 | openAITools[i] = ConvertToOpenAITool(tool) 78 | // Add tool to a map for lookup later 79 | toolsMap[tool.Name()] = tool 80 | 81 | } 82 | 83 | params := openai.ChatCompletionNewParams{ 84 | Messages: []openai.ChatCompletionMessageParamUnion{ 85 | openai.SystemMessage(systemPrompt), 86 | }, 87 | Tools: openAITools, 88 | Seed: openai.Int(0), 89 | Model: openai.ChatModelGPT4o, 90 | } 91 | 92 | for _, query := range queries { 93 | 94 | params.Messages = append(params.Messages, openai.UserMessage(query)) 95 | 96 | // Make initial chat completion request 97 | completion, err := openAIClient.Chat.Completions.New(ctx, params) 98 | if err != nil { 99 | panic(err) 100 | } 101 | 102 | toolCalls := completion.Choices[0].Message.ToolCalls 103 | 104 | // Return early if there are no tool calls 105 | if len(toolCalls) == 0 { 106 | log.Println("No function call") 107 | } 108 | 109 | // If there was a function call, continue the conversation 110 | params.Messages = append(params.Messages, completion.Choices[0].Message.ToParam()) 111 | for _, toolCall := range toolCalls { 112 | 113 | toolName := toolCall.Function.Name 114 | toolToInvoke := toolsMap[toolName] 115 | 116 | var args map[string]any 117 | err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | result, err := toolToInvoke.Invoke(ctx, args) 123 | if err != nil { 124 | log.Fatal("Could not invoke tool", err) 125 | } 126 | 127 | params.Messages = append(params.Messages, openai.ToolMessage(result.(string), toolCall.ID)) 128 | } 129 | 130 | completion, err = openAIClient.Chat.Completions.New(ctx, params) 131 | if err != nil { 132 | panic(err) 133 | } 134 | 135 | params.Messages = append(params.Messages, openai.AssistantMessage(query)) 136 | 137 | fmt.Println("\n", completion.Choices[0].Message.Content) 138 | 139 | } 140 | 141 | } 142 | ``` -------------------------------------------------------------------------------- /docs/en/getting-started/quickstart/shared/configure_toolbox.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- This file has been used in local_quickstart.md, local_quickstart_go.md & local_quickstart_js.md --> 2 | <!-- [START configure_toolbox] --> 3 | In this section, we will download Toolbox, configure our tools in a 4 | `tools.yaml`, and then run the Toolbox server. 5 | 6 | 1. Download the latest version of Toolbox as a binary: 7 | 8 | {{< notice tip >}} 9 | Select the 10 | [correct binary](https://github.com/googleapis/genai-toolbox/releases) 11 | corresponding to your OS and CPU architecture. 12 | {{< /notice >}} 13 | <!-- {x-release-please-start-version} --> 14 | ```bash 15 | export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64 16 | curl -O https://storage.googleapis.com/genai-toolbox/v0.17.0/$OS/toolbox 17 | ``` 18 | <!-- {x-release-please-end} --> 19 | 20 | 1. Make the binary executable: 21 | 22 | ```bash 23 | chmod +x toolbox 24 | ``` 25 | 26 | 1. Write the following into a `tools.yaml` file. Be sure to update any fields 27 | such as `user`, `password`, or `database` that you may have customized in the 28 | previous step. 29 | 30 | {{< notice tip >}} 31 | In practice, use environment variable replacement with the format ${ENV_NAME} 32 | instead of hardcoding your secrets into the configuration file. 33 | {{< /notice >}} 34 | 35 | ```yaml 36 | sources: 37 | my-pg-source: 38 | kind: postgres 39 | host: 127.0.0.1 40 | port: 5432 41 | database: toolbox_db 42 | user: ${USER_NAME} 43 | password: ${PASSWORD} 44 | tools: 45 | search-hotels-by-name: 46 | kind: postgres-sql 47 | source: my-pg-source 48 | description: Search for hotels based on name. 49 | parameters: 50 | - name: name 51 | type: string 52 | description: The name of the hotel. 53 | statement: SELECT * FROM hotels WHERE name ILIKE '%' || $1 || '%'; 54 | search-hotels-by-location: 55 | kind: postgres-sql 56 | source: my-pg-source 57 | description: Search for hotels based on location. 58 | parameters: 59 | - name: location 60 | type: string 61 | description: The location of the hotel. 62 | statement: SELECT * FROM hotels WHERE location ILIKE '%' || $1 || '%'; 63 | book-hotel: 64 | kind: postgres-sql 65 | source: my-pg-source 66 | description: >- 67 | Book a hotel by its ID. If the hotel is successfully booked, returns a NULL, raises an error if not. 68 | parameters: 69 | - name: hotel_id 70 | type: string 71 | description: The ID of the hotel to book. 72 | statement: UPDATE hotels SET booked = B'1' WHERE id = $1; 73 | update-hotel: 74 | kind: postgres-sql 75 | source: my-pg-source 76 | description: >- 77 | Update a hotel's check-in and check-out dates by its ID. Returns a message 78 | indicating whether the hotel was successfully updated or not. 79 | parameters: 80 | - name: hotel_id 81 | type: string 82 | description: The ID of the hotel to update. 83 | - name: checkin_date 84 | type: string 85 | description: The new check-in date of the hotel. 86 | - name: checkout_date 87 | type: string 88 | description: The new check-out date of the hotel. 89 | statement: >- 90 | UPDATE hotels SET checkin_date = CAST($2 as date), checkout_date = CAST($3 91 | as date) WHERE id = $1; 92 | cancel-hotel: 93 | kind: postgres-sql 94 | source: my-pg-source 95 | description: Cancel a hotel by its ID. 96 | parameters: 97 | - name: hotel_id 98 | type: string 99 | description: The ID of the hotel to cancel. 100 | statement: UPDATE hotels SET booked = B'0' WHERE id = $1; 101 | toolsets: 102 | my-toolset: 103 | - search-hotels-by-name 104 | - search-hotels-by-location 105 | - book-hotel 106 | - update-hotel 107 | - cancel-hotel 108 | ``` 109 | 110 | For more info on tools, check out the `Resources` section of the docs. 111 | 112 | 1. Run the Toolbox server, pointing to the `tools.yaml` file created earlier: 113 | 114 | ```bash 115 | ./toolbox --tools-file "tools.yaml" 116 | ``` 117 | 118 | {{< notice note >}} 119 | Toolbox enables dynamic reloading by default. To disable, use the 120 | `--disable-reload` flag. 121 | {{< /notice >}} 122 | <!-- [END configure_toolbox] --> ``` -------------------------------------------------------------------------------- /internal/prebuiltconfigs/tools/firestore.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 | firestore-source: 17 | kind: firestore 18 | project: ${FIRESTORE_PROJECT} 19 | database: ${FIRESTORE_DATABASE:} 20 | 21 | tools: 22 | get_documents: 23 | kind: firestore-get-documents 24 | source: firestore-source 25 | description: Gets multiple documents from Firestore by their paths 26 | add_documents: 27 | kind: firestore-add-documents 28 | source: firestore-source 29 | description: | 30 | Adds a new document to a Firestore collection. Please follow the best practices : 31 | 1. Always use typed values in the documentData: Every field must be wrapped with its appropriate type indicator (e.g., {"stringValue": "text"}) 32 | 2. Integer values can be strings in the documentData: The tool accepts integer values as strings (e.g., {"integerValue": "1500"}) 33 | 3. Use returnData sparingly: Only set to true when you need to verify the exact data that was written 34 | 4. Validate data before sending: Ensure your data matches Firestore's native JSON format 35 | 5. Handle timestamps properly: Use RFC3339 format for timestamp strings 36 | 6. Base64 encode binary data: Binary data must be base64 encoded in the bytesValue field 37 | 7. Consider security rules: Ensure your Firestore security rules allow document creation in the target collection 38 | update_document: 39 | kind: firestore-update-document 40 | source: firestore-source 41 | description: | 42 | Updates an existing document in Firestore. Supports both full document updates and selective field updates using an update mask. Please follow the best practices: 43 | 1. Use update masks for precision: When you only need to update specific fields, use the updateMask parameter to avoid unintended changes 44 | 2. Always use typed values in the documentData: Every field must be wrapped with its appropriate type indicator (e.g., {"stringValue": "text"}) 45 | 3. Delete fields using update mask: To delete fields, include them in the updateMask but omit them from documentData 46 | 4. Integer values can be strings: The tool accepts integer values as strings (e.g., {"integerValue": "1500"}) 47 | 5. Use returnData sparingly: Only set to true when you need to verify the exact data after the update 48 | 6. Handle timestamps properly: Use RFC3339 format for timestamp strings 49 | 7. Consider security rules: Ensure your Firestore security rules allow document updates 50 | list_collections: 51 | kind: firestore-list-collections 52 | source: firestore-source 53 | description: List Firestore collections for a given parent path 54 | delete_documents: 55 | kind: firestore-delete-documents 56 | source: firestore-source 57 | description: Delete multiple documents from Firestore 58 | query_collection: 59 | kind: firestore-query-collection 60 | source: firestore-source 61 | description: | 62 | Retrieves one or more Firestore documents from a collection in a database in the current project by a collection with a full document path. 63 | Use this if you know the exact path of a collection and the filtering clause you would like for the document. 64 | get_rules: 65 | kind: firestore-get-rules 66 | source: firestore-source 67 | description: Retrieves the active Firestore security rules for the current project 68 | validate_rules: 69 | kind: firestore-validate-rules 70 | source: firestore-source 71 | description: Checks the provided Firestore Rules source for syntax and validation errors. Provide the source code to validate. 72 | 73 | toolsets: 74 | firestore_database_tools: 75 | - get_documents 76 | - add_documents 77 | - update_document 78 | - list_collections 79 | - delete_documents 80 | - query_collection 81 | - get_rules 82 | - validate_rules 83 | ```