diff --git a/.ci/integration.cloudbuild.yaml b/.ci/integration.cloudbuild.yaml index 6bf5a335c9cd..733db8e438a2 100644 --- a/.ci/integration.cloudbuild.yaml +++ b/.ci/integration.cloudbuild.yaml @@ -276,7 +276,7 @@ steps: - "GOPATH=/gopath" - "BIGQUERY_PROJECT=$PROJECT_ID" - "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL" - secretEnv: ["CLIENT_ID"] + secretEnv: ["CLIENT_ID", "API_KEY"] volumes: - name: "go" path: "/gopath" @@ -1573,4 +1573,4 @@ substitutions: _MARIADB_PORT: "3307" _MARIADB_DATABASE: test_database _SNOWFLAKE_DATABASE: "test" - _SNOWFLAKE_SCHEMA: "PUBLIC" + _SNOWFLAKE_SCHEMA: "PUBLIC" \ No newline at end of file diff --git a/.github/workflows/deploy_dev_docs_to_cf.yaml b/.github/workflows/deploy_dev_docs_to_cf.yaml index 830d8fffdc11..cf56f4ca88de 100644 --- a/.github/workflows/deploy_dev_docs_to_cf.yaml +++ b/.github/workflows/deploy_dev_docs_to_cf.yaml @@ -49,7 +49,7 @@ jobs: - name: Setup Hugo uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3 with: - hugo-version: "0.145.0" + hugo-version: "0.160.0" extended: true - name: Setup Node diff --git a/.github/workflows/deploy_previous_version_docs_to_cf.yaml b/.github/workflows/deploy_previous_version_docs_to_cf.yaml index c41b125430c5..27d122979bc8 100644 --- a/.github/workflows/deploy_previous_version_docs_to_cf.yaml +++ b/.github/workflows/deploy_previous_version_docs_to_cf.yaml @@ -76,7 +76,7 @@ jobs: - name: Setup Hugo and Node uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3 with: - hugo-version: "0.145.0" + hugo-version: "0.160.0" extended: true - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: diff --git a/.github/workflows/deploy_versioned_docs_to_cf.yaml b/.github/workflows/deploy_versioned_docs_to_cf.yaml index 992b9222a962..1682413bf0d8 100644 --- a/.github/workflows/deploy_versioned_docs_to_cf.yaml +++ b/.github/workflows/deploy_versioned_docs_to_cf.yaml @@ -43,7 +43,7 @@ jobs: - name: Setup Hugo uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3 with: - hugo-version: "0.145.0" + hugo-version: "0.160.0" extended: true - name: Setup Node diff --git a/.github/workflows/docs_preview_build_cf.yaml b/.github/workflows/docs_preview_build_cf.yaml index 9242db8097ef..ffddb831ace7 100644 --- a/.github/workflows/docs_preview_build_cf.yaml +++ b/.github/workflows/docs_preview_build_cf.yaml @@ -47,7 +47,7 @@ jobs: - name: Setup Hugo uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3 with: - hugo-version: "0.145.0" + hugo-version: "0.160.0" extended: true - name: Setup Node diff --git a/.gitignore b/.gitignore index f67b283eba8f..98fa3d77c498 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ __pycache__/ mcp-toolbox toolbox toolbox.exe +**/test.db diff --git a/cmd/internal/config_test.go b/cmd/internal/config_test.go index 7fb0f090e0ff..8985494ed8d5 100644 --- a/cmd/internal/config_test.go +++ b/cmd/internal/config_test.go @@ -1877,7 +1877,7 @@ func TestPrebuiltTools(t *testing.T) { wantToolset: server.ToolsetConfigs{ "looker_dev_tools": tools.ToolsetConfig{ Name: "looker_dev_tools", - ToolNames: []string{"health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_project_directories", "create_project_directory", "delete_project_directory", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns", "get_lookml_tests", "run_lookml_tests", "create_view_from_table", "project_git_branch"}, + ToolNames: []string{"health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_project_directories", "create_project_directory", "delete_project_directory", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns", "get_lookml_tests", "run_lookml_tests", "create_view_from_table", "list_git_branches", "get_git_branch", "create_git_branch", "switch_git_branch", "delete_git_branch"}, }, }, }, diff --git a/cmd/internal/imports.go b/cmd/internal/imports.go index 9a24ed4aa647..e84192909a53 100644 --- a/cmd/internal/imports.go +++ b/cmd/internal/imports.go @@ -98,6 +98,7 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/dataproc/dataproclistjobs" _ "github.com/googleapis/mcp-toolbox/internal/tools/dgraph" _ "github.com/googleapis/mcp-toolbox/internal/tools/elasticsearch/elasticsearchesql" + _ "github.com/googleapis/mcp-toolbox/internal/tools/elasticsearch/elasticsearchexecuteesql" _ "github.com/googleapis/mcp-toolbox/internal/tools/firebird/firebirdexecutesql" _ "github.com/googleapis/mcp-toolbox/internal/tools/firebird/firebirdsql" _ "github.com/googleapis/mcp-toolbox/internal/tools/firestore/firestoreadddocuments" @@ -114,10 +115,12 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookeradddashboardfilter" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerconversationalanalytics" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateagent" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreategitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateprojectdirectory" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateprojectfile" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateviewfromtable" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteagent" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeletegitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteprojectdirectory" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteprojectfile" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdevmode" @@ -132,6 +135,7 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetdimensions" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetexplores" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetfilters" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetgitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetlookmltests" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetlooks" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetmeasures" @@ -141,11 +145,11 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetprojectfile" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetprojectfiles" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetprojects" - _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerhealthanalyze" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerhealthpulse" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerhealthvacuum" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistagents" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistgitbranches" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookermakedashboard" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookermakelook" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerquery" @@ -154,6 +158,7 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerrundashboard" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerrunlook" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerrunlookmltests" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerswitchgitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerupdateagent" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerupdateprojectfile" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookervalidateproject" diff --git a/docs/en/documentation/configuration/authentication/generic.md b/docs/en/documentation/configuration/authentication/generic.md index b5fa8d0c5a11..dd1b3268d300 100644 --- a/docs/en/documentation/configuration/authentication/generic.md +++ b/docs/en/documentation/configuration/authentication/generic.md @@ -19,26 +19,33 @@ your client ID or the intended audience for the token), the `authorizationServer` of your identity provider, and optionally a list of `scopesRequired` that must be present in the token's claims. -## Behavior +## Usage Modes -### Token Validation +The Generic Auth Service supports two distinct modes of operation: -When a request is received, the service will: +### 1. Toolbox Auth -1. Extract the token from the `_token` header (e.g., - `my-generic-auth_token`). -2. Fetch the JWKS from the configured `authorizationServer` (caching it in the - background) to verify the token's signature. -3. Validate that the token is not expired and its signature is valid. -4. Verify that the `aud` (audience) claim matches the configured `audience`. - claim contains all required scopes. -5. Return the validated claims to be used for [Authenticated - Parameters][auth-params] or [Authorized Invocations][auth-invoke]. +This mode is used for Toolbox's native authentication/authorization features. It +is active when you reference the auth service in a tool's configuration and +`mcpEnabled` is set to false. -[auth-invoke]: ../tools/_index.md#authorized-invocations -[auth-params]: ../tools/_index.md#authenticated-parameters +- **Header**: Expects the token in a custom header matching `_token` + (e.g., `my-generic-auth_token`). +- **Token Type**: Only supports **JWT** (OIDC) tokens. +- **Usage**: Used for [Authenticated Parameters][auth-params] and [Authorized + Invocations][auth-invoke]. + +#### Token Validation + +When a request is received in this mode, the service will: -## Example +1. Extract the token from the `_token` header. +2. Treat it as a JWT (opaque tokens are not supported in this mode). +3. Validates signature using JWKS fetched from `authorizationServer`. +4. Verifies expiration (`exp`) and audience (`aud`). +5. Verifies required scopes in `scope` claim. + +#### Example ```yaml kind: authServices @@ -46,7 +53,72 @@ name: my-generic-auth type: generic audience: ${YOUR_OIDC_AUDIENCE} authorizationServer: https://your-idp.example.com -mcpEnabled: false +# mcpEnabled: false +scopesRequired: + - read + - write +``` + +#### Tool Usage Example + +To use this auth service for **Authenticated Parameters** or **Authorized +Invocations**, reference it in your tool configuration: + +```yaml +kind: tool +name: secure_query +type: postgres-sql +source: my-pg-instance +statement: | + SELECT * FROM data WHERE user_id = $1 +parameters: + - name: user_id + type: strings + description: Auto-populated from token + authServices: + - name: my-generic-auth + field: sub # Extract 'sub' claim from JWT +authRequired: + - my-generic-auth # Require valid token for invocation +``` + +### 2. MCP Authorization + +This mode enforces global authentication for all MCP endpoints. It is active +when `mcpEnabled` is set to `true` in the auth service configuration. + +- **Header**: Expects the token in the standard `Authorization: Bearer ` + header. +- **Token Type**: Supports both **JWT** and **Opaque** tokens. +- **Usage**: Used to secure the entire MCP server. + +#### Token Validation + +When a request is received in this mode, the service will: + +1. Extract the token from the `Authorization` header after `Bearer ` prefix. +2. Determine if the token is a JWT or an opaque token based on format (JWTs + contain exactly two dots). +3. For **JWTs**: + - Validates signature using JWKS fetched from `authorizationServer`. + - Verifies expiration (`exp`) and audience (`aud`). + - Verifies required scopes in `scope` claim. +4. For **Opaque Tokens**: + - Calls the introspection endpoint (as listed in the `authorizationServer`'s + OIDC configuration). + - Verifies that the token is `active`. + - Verifies expiration (`exp`) and audience (`aud`). + - Verifies required scopes in `scope` field. + +#### Example + +```yaml +kind: authServices +name: my-generic-auth +type: generic +audience: ${YOUR_TOKEN_AUDIENCE} +authorizationServer: https://your-idp.example.com +mcpEnabled: true scopesRequired: - read - write @@ -56,6 +128,11 @@ scopesRequired: ${ENV_NAME} instead of hardcoding your secrets into the configuration file. {{< /notice >}} +[auth-invoke]: ../tools/_index.md#authorized-invocations +[auth-params]: ../tools/_index.md#authenticated-parameters +[mcp-auth]: + https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization + ## Reference | **field** | **type** | **required** | **description** | diff --git a/docs/en/documentation/configuration/pre-post-processing/go/adk/go.mod b/docs/en/documentation/configuration/pre-post-processing/go/adk/go.mod index 9b002abc505f..dfbc29bba89b 100644 --- a/docs/en/documentation/configuration/pre-post-processing/go/adk/go.mod +++ b/docs/en/documentation/configuration/pre-post-processing/go/adk/go.mod @@ -1,6 +1,6 @@ module example.com/adk-agent -go 1.24.4 +go 1.25.0 require ( github.com/googleapis/mcp-toolbox-sdk-go v0.5.1 @@ -26,14 +26,14 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/sdk v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.33.0 // indirect google.golang.org/api v0.263.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect diff --git a/docs/en/documentation/configuration/pre-post-processing/go/adk/go.sum b/docs/en/documentation/configuration/pre-post-processing/go/adk/go.sum index ac42cf4fbd4f..0ef8cb0577bd 100644 --- a/docs/en/documentation/configuration/pre-post-processing/go/adk/go.sum +++ b/docs/en/documentation/configuration/pre-post-processing/go/adk/go.sum @@ -78,16 +78,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= @@ -98,8 +98,8 @@ golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= diff --git a/docs/en/documentation/configuration/pre-post-processing/js/langchain/package-lock.json b/docs/en/documentation/configuration/pre-post-processing/js/langchain/package-lock.json index bedfb41d5b76..cbbb2422e871 100644 --- a/docs/en/documentation/configuration/pre-post-processing/js/langchain/package-lock.json +++ b/docs/en/documentation/configuration/pre-post-processing/js/langchain/package-lock.json @@ -178,14 +178,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/base64-js": { @@ -1054,10 +1054,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/safe-buffer": { "version": "5.2.1", diff --git a/docs/en/documentation/connect-to/ides/looker_mcp.md b/docs/en/documentation/connect-to/ides/looker_mcp.md index 4688fe5c156e..20c2282457bd 100644 --- a/docs/en/documentation/connect-to/ides/looker_mcp.md +++ b/docs/en/documentation/connect-to/ides/looker_mcp.md @@ -551,7 +551,11 @@ as well as get the database schema needed to write LookML effectively. 1. **get_lookml_tests**: Retrieves a list of available LookML tests for a project. 1. **run_lookml_tests**: Executes specific LookML tests within a project. 1. **create_view_from_table**: Generates boilerplate LookML views directly from the database schema. -1. **project_git_branch**: Fetch and manipulate the git branch of a LookML project. +1. **list_git_branches**: List the available git branches of a LookML project. +1. **get_git_branch**: Get the current git branch of a LookML project. +1. **create_git_branch**: Create a new git branch for a LookML project. +1. **switch_git_branch**: Switch the git branch of a LookML project. +1. **delete_git_branch**: Delete a git branch of a LookML project. {{< notice note >}} Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs diff --git a/docs/en/integrations/bigquery/tools/bigquery-sql.md b/docs/en/integrations/bigquery/tools/bigquery-sql.md index 0ee1f7a2e804..f430b436eb50 100644 --- a/docs/en/integrations/bigquery/tools/bigquery-sql.md +++ b/docs/en/integrations/bigquery/tools/bigquery-sql.md @@ -29,18 +29,18 @@ The behavior of this tool is influenced by the `writeMode` setting on its BigQuery uses [GoogleSQL][bigquery-googlesql] for querying data. The integration with Toolbox supports this dialect. The specified SQL statement is executed, and -parameters can be inserted into the query. BigQuery supports both named parameters -(e.g., `@name`) and positional parameters (`?`), but they cannot be mixed in the -same query. +parameters can be inserted into the query. BigQuery supports both named +parameters (e.g., `@name`) and positional parameters (`?`), but they cannot be +mixed in the same query. [bigquery-googlesql]: - https://cloud.google.com/bigquery/docs/reference/standard-sql/ + https://cloud.google.com/bigquery/docs/reference/standard-sql/ ## Example -> **Note:** This tool uses [parameterized -> queries](https://cloud.google.com/bigquery/docs/parameterized-queries) to -> prevent SQL injections. Query parameters can be used as substitutes for +> **Note:** This tool uses +> [parameterized queries](https://cloud.google.com/bigquery/docs/parameterized-queries) +> to prevent SQL injections. Query parameters can be used as substitutes for > arbitrary expressions. Parameters cannot be used as substitutes for > identifiers, column names, table names, or other parts of the query. @@ -77,13 +77,93 @@ parameters: description: Email address of the user ``` +### Example with Vector Search + +BigQuery supports vector similarity search using the `ML.DISTANCE` function. +When using an embeddingModel with a `bigquery-sql` tool, the tool automatically +converts text parameters into the native ARRAY format required by +BigQuery. + +#### Define the Embedding Model + +See +[EmbeddingModels](../../../documentation/configuration/embedding-models/_index.md) +for more information. + +```yaml +kind: embeddingModel +name: gemini-model +type: gemini +model: gemini-embedding-001 +apiKey: ${GOOGLE_API_KEY} +dimension: 768 +``` + +#### Vector Ingestion Tool + +This tool stores both the raw text and its vector representation. It uses +`valueFromParam` to hide the vector conversion logic from the LLM, ensuring the +Agent only has to provide the content once. + +```yaml +kind: tool +name: insert_doc +type: bigquery-sql +source: my-bigquery-source +statement: | + INSERT INTO `my-project.my-dataset.vector_table` (id, content, embedding) + VALUES (1, @content, @text_to_embed) +description: | + Internal tool to index new documents for future search. +parameters: + - name: content + type: string + description: The text content to store. + - name: text_to_embed + type: string + # Automatically copies 'content' and converts it to a FLOAT64 array + valueFromParam: content + embeddedBy: gemini-model +``` + +#### Vector Search Tool + +This tool allows the Agent to perform a natural language search. The query +string provided by the Agent is converted into a vector before the SQL is +executed. + +```yaml +kind: tool +name: search_docs +type: bigquery-sql +source: my-bigquery-source +statement: | + SELECT + id, + content, + ML.DISTANCE(embedding, @query, 'COSINE') AS distance + FROM + `my-project.my-dataset.vector_table` + ORDER BY + distance + LIMIT 1 +description: | + Search for documents using natural language. + Returns the most semantically similar result. +parameters: + - name: query + type: string + description: The search terms or question. + embeddedBy: gemini-model +``` + ### Example with Template Parameters > **Note:** This tool allows direct modifications to the SQL statement, > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](../#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -106,11 +186,11 @@ templateParameters: ## Reference -| **field** | **type** | **required** | **description** | -|--------------------|:---------------------------------------------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------| -| type | string | true | Must be "bigquery-sql". | -| source | string | true | Name of the source the GoogleSQL should execute on. | -| description | string | true | Description of the tool that is passed to the LLM. | -| statement | string | true | The GoogleSQL statement to execute. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](../#template-parameters) | false | List of [templateParameters](../#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | \ No newline at end of file +| **field** | **type** | **required** | **description** | +| ------------------ | :--------------------------------------------------------------------------------------------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | string | true | Must be "bigquery-sql". | +| source | string | true | Name of the source the GoogleSQL should execute on. | +| description | string | true | Description of the tool that is passed to the LLM. | +| statement | string | true | The GoogleSQL statement to execute. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/bigtable/tools/bigtable-sql.md b/docs/en/integrations/bigtable/tools/bigtable-sql.md index e3fd470c7a48..62035cd3f6eb 100644 --- a/docs/en/integrations/bigtable/tools/bigtable-sql.md +++ b/docs/en/integrations/bigtable/tools/bigtable-sql.md @@ -77,7 +77,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -106,8 +106,8 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | ## Advanced Usage diff --git a/docs/en/integrations/cassandra/tools/cassandra-cql.md b/docs/en/integrations/cassandra/tools/cassandra-cql.md index e1d4932d007b..2569b70efc74 100644 --- a/docs/en/integrations/cassandra/tools/cassandra-cql.md +++ b/docs/en/integrations/cassandra/tools/cassandra-cql.md @@ -61,7 +61,7 @@ parameters: > including keyspaces, table names, and column names. **This makes it more > vulnerable to CQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](../#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -95,5 +95,5 @@ templateParameters: | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | CQL statement to execute. | | authRequired | []string | false | List of authentication requirements for the source. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the CQL statement. | -| templateParameters | [templateParameters](../#template-parameters) | false | List of [templateParameters](../#template-parameters) that will be inserted into the CQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the CQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the CQL statement before executing prepared statement. | diff --git a/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcloneinstance.md b/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcloneinstance.md index 9d158ece0b1d..76302027cb87 100644 --- a/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcloneinstance.md +++ b/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcloneinstance.md @@ -66,5 +66,5 @@ description: "Creates an exact copy of a Cloud SQL instance at a specific point ## Additional Resources - [Cloud SQL Admin API documentation](https://cloud.google.com/sql/docs/mysql/admin-api) -- [Toolbox Cloud SQL tools documentation](_index.md) -- [Cloud SQL Clone API documentation](https://cloud.google.com/sql/docs/mysql/clone-instance) \ No newline at end of file +- [Toolbox Cloud SQL tools documentation](../source.md) +- [Cloud SQL Clone API documentation](https://cloud.google.com/sql/docs/mysql/clone-instance) diff --git a/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcreatebackup.md b/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcreatebackup.md index a6908eb6feae..6c5b26047f32 100644 --- a/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcreatebackup.md +++ b/docs/en/integrations/cloud-sql-admin/tools/cloudsqlcreatebackup.md @@ -43,5 +43,5 @@ description: "Creates a backup on the given Cloud SQL instance." ## Additional Resources - [Cloud SQL Admin API documentation](https://cloud.google.com/sql/docs/mysql/admin-api) -- [Toolbox Cloud SQL tools documentation](_index.md) -- [Cloud SQL Backup API documentation](https://cloud.google.com/sql/docs/mysql/backup-recovery/backups) \ No newline at end of file +- [Toolbox Cloud SQL tools documentation](../source.md) +- [Cloud SQL Backup API documentation](https://cloud.google.com/sql/docs/mysql/backup-recovery/backups) diff --git a/docs/en/integrations/cloud-sql-admin/tools/cloudsqlrestorebackup.md b/docs/en/integrations/cloud-sql-admin/tools/cloudsqlrestorebackup.md index 033795c35a1d..d595b0689d14 100644 --- a/docs/en/integrations/cloud-sql-admin/tools/cloudsqlrestorebackup.md +++ b/docs/en/integrations/cloud-sql-admin/tools/cloudsqlrestorebackup.md @@ -50,5 +50,5 @@ description: "Restores a backup onto the given Cloud SQL instance." ## Additional Resources - [Cloud SQL Admin API documentation](https://cloud.google.com/sql/docs/mysql/admin-api) -- [Toolbox Cloud SQL tools documentation](_index.md) +- [Toolbox Cloud SQL tools documentation](../source.md) - [Cloud SQL Restore API documentation](https://cloud.google.com/sql/docs/mysql/backup-recovery/restoring) diff --git a/docs/en/integrations/cockroachdb/tools/cockroachdb-execute-sql.md b/docs/en/integrations/cockroachdb/tools/cockroachdb-execute-sql.md index 2525c1939778..ba0cee5a5384 100644 --- a/docs/en/integrations/cockroachdb/tools/cockroachdb-execute-sql.md +++ b/docs/en/integrations/cockroachdb/tools/cockroachdb-execute-sql.md @@ -157,7 +157,7 @@ This tool is ideal for: - Debugging and troubleshooting - Schema inspection -For production use cases, use [cockroachdb-sql](_index.md) with parameterized queries. +For production use cases, use [cockroachdb-sql](./cockroachdb-sql.md) with parameterized queries. #### Be Cautious with Data Modification @@ -273,8 +273,8 @@ The tool will return descriptive errors for: ## Additional Resources -- [cockroachdb-sql](_index.md) - For parameterized, production-ready queries +- [cockroachdb-sql](./cockroachdb-sql.md) - For parameterized, production-ready queries - [cockroachdb-list-tables](./cockroachdb-list-tables.md) - List tables in the database - [cockroachdb-list-schemas](./cockroachdb-list-schemas.md) - List database schemas -- [CockroachDB Source](_index.md) - Source configuration reference +- [CockroachDB Source](../source.md) - Source configuration reference - [CockroachDB SQL Reference](https://www.cockroachlabs.com/docs/stable/sql-statements.html) - Official SQL documentation diff --git a/docs/en/integrations/cockroachdb/tools/cockroachdb-list-schemas.md b/docs/en/integrations/cockroachdb/tools/cockroachdb-list-schemas.md index ea578dba0a1e..54b9399e0bed 100644 --- a/docs/en/integrations/cockroachdb/tools/cockroachdb-list-schemas.md +++ b/docs/en/integrations/cockroachdb/tools/cockroachdb-list-schemas.md @@ -306,8 +306,8 @@ The tool handles common errors: ## Additional Resources -- [cockroachdb-sql](_index.md) - Execute parameterized queries +- [cockroachdb-sql](./cockroachdb-sql.md) - Execute parameterized queries - [cockroachdb-execute-sql](./cockroachdb-execute-sql.md) - Execute ad-hoc SQL - [cockroachdb-list-tables](./cockroachdb-list-tables.md) - List tables in the database -- [CockroachDB Source](_index.md) - Source configuration reference +- [CockroachDB Source](../source.md) - Source configuration reference - [CockroachDB Schema Design](https://www.cockroachlabs.com/docs/stable/schema-design-overview.html) - Official documentation diff --git a/docs/en/integrations/cockroachdb/tools/cockroachdb-list-tables.md b/docs/en/integrations/cockroachdb/tools/cockroachdb-list-tables.md index 2f19d81991b8..e32ae522da01 100644 --- a/docs/en/integrations/cockroachdb/tools/cockroachdb-list-tables.md +++ b/docs/en/integrations/cockroachdb/tools/cockroachdb-list-tables.md @@ -345,8 +345,8 @@ The tool handles common errors: ## Additional Resources -- [cockroachdb-sql](_index.md) - Execute parameterized queries +- [cockroachdb-sql](./cockroachdb-sql.md) - Execute parameterized queries - [cockroachdb-execute-sql](./cockroachdb-execute-sql.md) - Execute ad-hoc SQL - [cockroachdb-list-schemas](./cockroachdb-list-schemas.md) - List database schemas -- [CockroachDB Source](_index.md) - Source configuration reference +- [CockroachDB Source](../source.md) - Source configuration reference - [CockroachDB Schema Design](https://www.cockroachlabs.com/docs/stable/schema-design-overview.html) - Best practices diff --git a/docs/en/integrations/cockroachdb/tools/cockroachdb-sql.md b/docs/en/integrations/cockroachdb/tools/cockroachdb-sql.md index 12f4bee70f2a..7805c690d263 100644 --- a/docs/en/integrations/cockroachdb/tools/cockroachdb-sql.md +++ b/docs/en/integrations/cockroachdb/tools/cockroachdb-sql.md @@ -292,4 +292,4 @@ The tool automatically handles: - [cockroachdb-execute-sql](./cockroachdb-execute-sql.md) - For ad-hoc SQL execution - [cockroachdb-list-tables](./cockroachdb-list-tables.md) - List tables in the database - [cockroachdb-list-schemas](./cockroachdb-list-schemas.md) - List database schemas -- [CockroachDB Source](_index.md) - Source configuration reference +- [CockroachDB Source](../source.md) - Source configuration reference diff --git a/docs/en/integrations/couchbase/tools/couchbase-sql.md b/docs/en/integrations/couchbase/tools/couchbase-sql.md index 11a125b5c150..cc97ae87621f 100644 --- a/docs/en/integrations/couchbase/tools/couchbase-sql.md +++ b/docs/en/integrations/couchbase/tools/couchbase-sql.md @@ -67,7 +67,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -96,6 +96,6 @@ templateParameters: | source | string | true | Name of the source the SQL query should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be used with the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be used with the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | | authRequired | array[string] | false | List of auth services that are required to use this tool. | diff --git a/docs/en/integrations/dgraph/tools/dgraph-dql.md b/docs/en/integrations/dgraph/tools/dgraph-dql.md index 738a795fcac2..42c5a0bc42fb 100644 --- a/docs/en/integrations/dgraph/tools/dgraph-dql.md +++ b/docs/en/integrations/dgraph/tools/dgraph-dql.md @@ -132,4 +132,4 @@ parameters: | statement | string | true | dql statement to execute | | isQuery | boolean | false | To run statement as query set true otherwise false | | timeout | string | false | To set timeout for query | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be used with the dql statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be used with the dql statement. | diff --git a/docs/en/integrations/elasticsearch/tools/elasticsearch-esql.md b/docs/en/integrations/elasticsearch/tools/elasticsearch-esql.md index 91ce1cffc50d..ac8d3819ef47 100644 --- a/docs/en/integrations/elasticsearch/tools/elasticsearch-esql.md +++ b/docs/en/integrations/elasticsearch/tools/elasticsearch-esql.md @@ -13,23 +13,14 @@ Execute ES|QL queries. This tool allows you to execute ES|QL queries against your Elasticsearch cluster. You can use this to perform complex searches and aggregations. -See the [official -documentation](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started) +See the +[official documentation](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started) for more information. ## Compatible Sources {{< compatible-sources >}} -## Parameters - -| **name** | **type** | **required** | **description** | -|------------|:---------------------------------------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| query | string | false | The ES\|QL query to run. Can also be passed by parameters. | -| format | string | false | The format of the query. Default is json. Valid values are csv, json, tsv, txt, yaml, cbor, smile, or arrow. | -| timeout | integer | false | The timeout for the query in seconds. Default is 60 (1 minute). | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be used with the ES\|QL query.
Only supports “string”, “integer”, “float”, “boolean”. | - ## Example ```yaml @@ -48,3 +39,12 @@ parameters: description: Limit the number of results. required: true ``` + +## Reference + +| **field** | **type** | **required** | **description** | +| ---------- | :-------------------------------------: | :----------: | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| query | string | false | The ES\|QL query to run. Can also be passed by parameters. | +| format | string | false | The format of the query. Default is json. Valid values are `csv`, `json`, `tsv`, `txt`, `yaml`, `cbor`, `smile`, or `arrow`. | +| timeout | integer | false | The timeout for the query in seconds. Default is 60 (1 minute). | +| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be used with the ES\|QL query.
Only supports “string”, “integer”, “float”, “boolean”. | diff --git a/docs/en/integrations/elasticsearch/tools/elasticsearch-execute-esql.md b/docs/en/integrations/elasticsearch/tools/elasticsearch-execute-esql.md new file mode 100644 index 000000000000..852f3c30e5a1 --- /dev/null +++ b/docs/en/integrations/elasticsearch/tools/elasticsearch-execute-esql.md @@ -0,0 +1,50 @@ +--- +title: "elasticsearch-execute-esql" +type: docs +weight: 3 +description: > + Execute arbitrary ES|QL statements. +--- + +## About + +Execute arbitrary ES|QL statements. + +This tool allows you to execute arbitrary ES|QL statements against your +Elasticsearch cluster at runtime. This is useful for ad-hoc queries where the +statement is not known beforehand. + +See the +[official documentation](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started) +for more information. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +| **name** | **type** | **required** | **description** | +| -------- | :------: | :----------: | -------------------------------- | +| query | string | true | The ES|QL statement to execute. | + +## Example + +```yaml +kind: tool +name: execute_ad_hoc_esql +type: elasticsearch-execute-esql +source: elasticsearch-source +description: Use this tool to execute arbitrary ES|QL statements. +format: json +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------| +| type | string | true | Must be "elasticsearch-execute-esql". | +| source | string | true | Name of the source the ES|QL should execute on. | +| description | string | true | Description of the tool that is passed to the LLM. | +| format | string | false | The format of the query. Default is json. Valid values are `csv`, `json`, `tsv`, `txt`, `yaml`, `cbor`, `smile`, or `arrow`. | + diff --git a/docs/en/integrations/firebird/tools/firebird-sql.md b/docs/en/integrations/firebird/tools/firebird-sql.md index 4e6cb2c2b12d..cd6ba0d6a218 100644 --- a/docs/en/integrations/firebird/tools/firebird-sql.md +++ b/docs/en/integrations/firebird/tools/firebird-sql.md @@ -102,7 +102,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](../#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -131,5 +131,5 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](../#template-parameters) | false | List of [templateParameters](../#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/firestore/tools/firestore-query.md b/docs/en/integrations/firestore/tools/firestore-query.md index 97f824d593c0..3bf14e295ded 100644 --- a/docs/en/integrations/firestore/tools/firestore-query.md +++ b/docs/en/integrations/firestore/tools/firestore-query.md @@ -432,6 +432,6 @@ curl -X POST http://localhost:5000/api/tool/your-tool-name/invoke \ - [firestore-query-collection](firestore-query-collection.md) - Non-parameterizable query tool -- [Firestore Source Configuration](_index.md) +- [Firestore Source Configuration](../source.md) - [Firestore Query Documentation](https://firebase.google.com/docs/firestore/query-data/queries) diff --git a/docs/en/integrations/http/tools/http-tool.md b/docs/en/integrations/http/tools/http-tool.md index 26ee59bb275a..69559d0dda70 100644 --- a/docs/en/integrations/http/tools/http-tool.md +++ b/docs/en/integrations/http/tools/http-tool.md @@ -266,8 +266,8 @@ headerParams: | method | string | true | The HTTP method to use (e.g., GET, POST, PUT, DELETE). | | headers | map[string]string | false | A map of headers to include in the HTTP request (overrides source headers). | | requestBody | string | false | The request body payload. Use [go template][go-template-doc] with the parameter name as the placeholder (e.g., `{{.id}}` will be replaced with the value of the parameter that has name `id` in the `bodyParams` section). | -| queryParams | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the query string. | -| bodyParams | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the request body payload. | -| headerParams | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted as the request headers. | +| queryParams | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the query string. | +| bodyParams | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the request body payload. | +| headerParams | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted as the request headers. | [go-template-doc]: diff --git a/docs/en/integrations/looker/prebuilt-configs/looker-dev.md b/docs/en/integrations/looker/prebuilt-configs/looker-dev.md index d847ca6bfe93..ee2469db7849 100644 --- a/docs/en/integrations/looker/prebuilt-configs/looker-dev.md +++ b/docs/en/integrations/looker/prebuilt-configs/looker-dev.md @@ -43,4 +43,8 @@ description: "Details of the Looker Dev prebuilt configuration." * `get_lookml_tests`: Retrieves a list of available LookML tests for a project. * `run_lookml_tests`: Executes specific LookML tests within a project. * `create_view_from_table`: Generates boilerplate LookML views directly from the database schema. - * `project_git_branch`: Fetch and manipulate the git branch of a LookML project. + * `list_git_branches`: List the available git branches of a LookML project. + * `get_git_branch`: Get the current git branch of a LookML project. + * `create_git_branch`: Create a new git branch for a LookML project. + * `switch_git_branch`: Switch the git branch of a LookML project. + * `delete_git_branch`: Delete a git branch of a LookML project. diff --git a/docs/en/integrations/looker/tools/looker-create-git-branch.md b/docs/en/integrations/looker/tools/looker-create-git-branch.md new file mode 100644 index 000000000000..7e4b18252c06 --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-create-git-branch.md @@ -0,0 +1,44 @@ +--- +title: "Create Git Branch Tool" +type: docs +weight: 1 +description: > + A "looker-create-git-branch" tool is used to create a new git branch for a LookML project. +--- + +## About + +A `looker-create-git-branch` tool is used to create a new git branch +for a LookML project. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +| **field** | **type** | **required** | **description** | +| ---------- | :------: | :----------: | ----------------------------------------- | +| project_id | string | true | The unique ID of the LookML project. | +| branch | string | true | The git branch to create. | +| ref | string | false | The ref to use as the start of a new branch. Defaults to HEAD of current branch. | + +## Example + +```yaml +kind: tool +name: create_project_git_branch +type: looker-create-git-branch +source: looker-source +description: | + This tool is used to create a new git branch of a LookML + project. This only works in dev mode. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-create-git-branch". | +| source | string | true | Name of the source. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-delete-git-branch.md b/docs/en/integrations/looker/tools/looker-delete-git-branch.md new file mode 100644 index 000000000000..da9d42b2abdd --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-delete-git-branch.md @@ -0,0 +1,43 @@ +--- +title: "Delete Git Branch Tool" +type: docs +weight: 1 +description: > + A "looker-delete-git-branch" tool is used to delete a git branch of a LookML project. +--- + +## About + +A `looker-delete-git-branch` tool is used to delete a git branch +of a LookML project. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +| **field** | **type** | **required** | **description** | +| ---------- | :------: | :----------: | ----------------------------------------- | +| project_id | string | true | The unique ID of the LookML project. | +| branch | string | true | The git branch to delete. | + +## Example + +```yaml +kind: tool +name: delete_project_git_branch +type: looker-delete-git-branch +source: looker-source +description: | + This tool is used to delete a git branch of a LookML + project. This only works in dev mode. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-delete-git-branch". | +| source | string | true | Name of the source. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-get-git-branch.md b/docs/en/integrations/looker/tools/looker-get-git-branch.md new file mode 100644 index 000000000000..00052ea5c685 --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-get-git-branch.md @@ -0,0 +1,42 @@ +--- +title: "Get Git Branch Tool" +type: docs +weight: 1 +description: > + A "looker-get-git-branch" tool is used to retrieve the current git branch of a LookML project. +--- + +## About + +A `looker-get-git-branch` tool is used to retrieve the current git branch +of a LookML project. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +| **field** | **type** | **required** | **description** | +| ---------- | :------: | :----------: | ----------------------------------------- | +| project_id | string | true | The unique ID of the LookML project. | + +## Example + +```yaml +kind: tool +name: get_project_git_branch +type: looker-get-git-branch +source: looker-source +description: | + This tool is used to retrieve the current git branch of a LookML + project. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-get-git-branch". | +| source | string | true | Name of the source. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-git-branch.md b/docs/en/integrations/looker/tools/looker-git-branch.md deleted file mode 100644 index 0a5cc2e84467..000000000000 --- a/docs/en/integrations/looker/tools/looker-git-branch.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "looker-git-branch" -type: docs -weight: 1 -description: > - A "looker-git-branch" tool is used to retrieve and manipulate the git branch - of a LookML project. ---- - -## About - -A `looker-git-branch` tool is used to retrieve and manipulate the git branch -of a LookML project. - -`looker-git-branch` requires two parameters, the LookML `project_id` and the -`operation`. The operation must be one of `list`, `get`, `create`, `switch`, -or `delete`. - -The `list` operation retrieves the list of available branches. The `get` -operation retrieves the current branch. - -`create`, `switch` and `delete` all require an additional parameter, the -`branch` name. The `create` operation creates a new branch. The `switch` -operation switches to the specified branch. The `delete` operation deletes -the specified branch. - -`create` and `switch` can both use an additional `ref` parameter, which is -the git ref that the branch should be at. If it isn't specified, `create` -will start with the ref of the current branch. `switch` will start with the -HEAD of that branch. Specifying `ref` will do the equivalent of `reset --hard` -on the branch. - -## Compatible Sources - -{{< compatible-sources >}} - -## Example -```yaml -kind: tool -name: project_git_branch -type: looker-git-branch -source: looker-source -description: | - This tool is used to retrieve and manipulate the git branch of a LookML - project. - - An operation id must be provided which is one of the following: - * `list` - List all the available branch names. - * `get` - Get the current branch name. - * `create` - Create a new branch. The branch is initial set to the current - ref, unless ref is specified. This only works in dev mode. - * `switch` - Change the branch to the given branch, and update to the given - ref if specified. This only works in dev mode. - * `delete` - Delete a branch. This only works in dev mode. - - Parameters: - - project_id (required): The unique ID of the LookML project. - - operation (required): One of `list`, `get`, `create`, `switch`, or `delete`. - - branch (optional): The branch to create, switch to, or delete. - - ref (optional): The ref to start a newly created branch, or change a branch - with `reset --hard` on a switch operation. -``` - -## Reference - -| **field** | **type** | **required** | **description** | -|-------------|:--------:|:------------:|----------------------------------------------------| -| type | string | true | Must be "looker-git-branch". | -| source | string | true | Name of the source the SQL should execute on. | -| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-list-git-branches.md b/docs/en/integrations/looker/tools/looker-list-git-branches.md new file mode 100644 index 000000000000..160f2666da3e --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-list-git-branches.md @@ -0,0 +1,42 @@ +--- +title: "List Git Branches Tool" +type: docs +weight: 1 +description: > + A "looker-list-git-branches" tool is used to retrieve the list of available git branches of a LookML project. +--- + +## About + +A `looker-list-git-branches` tool is used to retrieve the list of available git branches +of a LookML project. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +| **field** | **type** | **required** | **description** | +| ---------- | :------: | :----------: | ----------------------------------------- | +| project_id | string | true | The unique ID of the LookML project. | + +## Example + +```yaml +kind: tool +name: list_project_git_branches +type: looker-list-git-branches +source: looker-source +description: | + This tool is used to retrieve the list of available git branches of a LookML + project. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-list-git-branches". | +| source | string | true | Name of the source. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-switch-git-branch.md b/docs/en/integrations/looker/tools/looker-switch-git-branch.md new file mode 100644 index 000000000000..5f490840b8a7 --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-switch-git-branch.md @@ -0,0 +1,44 @@ +--- +title: "Switch Git Branch Tool" +type: docs +weight: 1 +description: > + A "looker-switch-git-branch" tool is used to switch the git branch of a LookML project. +--- + +## About + +A `looker-switch-git-branch` tool is used to switch the git branch +of a LookML project. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +| **field** | **type** | **required** | **description** | +| ---------- | :------: | :----------: | ----------------------------------------- | +| project_id | string | true | The unique ID of the LookML project. | +| branch | string | true | The git branch to switch to. | +| ref | string | false | The ref to switch the branch to using `reset --hard`. | + +## Example + +```yaml +kind: tool +name: switch_project_git_branch +type: looker-switch-git-branch +source: looker-source +description: | + This tool is used to switch the git branch of a LookML + project. This only works in dev mode. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-switch-git-branch". | +| source | string | true | Name of the source. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/mindsdb/tools/mindsdb-sql.md b/docs/en/integrations/mindsdb/tools/mindsdb-sql.md index 7784250bb981..b53ae3aa7519 100644 --- a/docs/en/integrations/mindsdb/tools/mindsdb-sql.md +++ b/docs/en/integrations/mindsdb/tools/mindsdb-sql.md @@ -81,7 +81,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](../#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -170,5 +170,5 @@ ORDER BY created_at DESC; | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](../#template-parameters) | false | List of [templateParameters](../#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/mssql/tools/mssql-sql.md b/docs/en/integrations/mssql/tools/mssql-sql.md index 8cbd81ea29b5..b3a2cd641d5c 100644 --- a/docs/en/integrations/mssql/tools/mssql-sql.md +++ b/docs/en/integrations/mssql/tools/mssql-sql.md @@ -79,7 +79,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -108,5 +108,5 @@ templateParameters: | source | string | true | Name of the source the T-SQL statement should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/mysql/tools/mysql-sql.md b/docs/en/integrations/mysql/tools/mysql-sql.md index ae4cd7a89957..dd412d3e5d43 100644 --- a/docs/en/integrations/mysql/tools/mysql-sql.md +++ b/docs/en/integrations/mysql/tools/mysql-sql.md @@ -72,7 +72,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -101,5 +101,5 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/neo4j/tools/neo4j-cypher.md b/docs/en/integrations/neo4j/tools/neo4j-cypher.md index 4c99c68a8024..aef621125550 100644 --- a/docs/en/integrations/neo4j/tools/neo4j-cypher.md +++ b/docs/en/integrations/neo4j/tools/neo4j-cypher.md @@ -75,4 +75,4 @@ parameters: | source | string | true | Name of the source the Cypher query should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | Cypher statement to execute | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be used with the Cypher statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be used with the Cypher statement. | diff --git a/docs/en/integrations/oceanbase/tools/oceanbase-sql.md b/docs/en/integrations/oceanbase/tools/oceanbase-sql.md index f972911348b1..56ee9fcf63d1 100644 --- a/docs/en/integrations/oceanbase/tools/oceanbase-sql.md +++ b/docs/en/integrations/oceanbase/tools/oceanbase-sql.md @@ -125,5 +125,5 @@ parameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](..#specifying-parameters) | false | List of [parameters](..#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/singlestore/tools/singlestore-sql.md b/docs/en/integrations/singlestore/tools/singlestore-sql.md index 022707e9d7e1..3cf5e394f651 100644 --- a/docs/en/integrations/singlestore/tools/singlestore-sql.md +++ b/docs/en/integrations/singlestore/tools/singlestore-sql.md @@ -70,7 +70,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -99,5 +99,5 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/snowflake/tools/snowflake-sql.md b/docs/en/integrations/snowflake/tools/snowflake-sql.md index 2a461ebbcac6..b384207fcaa9 100644 --- a/docs/en/integrations/snowflake/tools/snowflake-sql.md +++ b/docs/en/integrations/snowflake/tools/snowflake-sql.md @@ -71,7 +71,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -100,6 +100,6 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | | authRequired | array[string] | false | List of auth services that are required to use this tool. | diff --git a/docs/en/integrations/spanner/tools/spanner-sql.md b/docs/en/integrations/spanner/tools/spanner-sql.md index 70e058ffc40a..bf56e98cd662 100644 --- a/docs/en/integrations/spanner/tools/spanner-sql.md +++ b/docs/en/integrations/spanner/tools/spanner-sql.md @@ -133,7 +133,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -162,6 +162,6 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | | readOnly | bool | false | When set to `true`, the `statement` is run as a read-only transaction. Default: `false`. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/sqlite/tools/sqlite-sql.md b/docs/en/integrations/sqlite/tools/sqlite-sql.md index a5e0799a96ef..9822c76f6dec 100644 --- a/docs/en/integrations/sqlite/tools/sqlite-sql.md +++ b/docs/en/integrations/sqlite/tools/sqlite-sql.md @@ -50,7 +50,7 @@ statement: SELECT * FROM users WHERE name LIKE ? AND age >= ? > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -79,5 +79,5 @@ templateParameters: | source | string | true | Name of the source the SQLite source configuration. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | The SQL statement to execute. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/tidb/tools/tidb-sql.md b/docs/en/integrations/tidb/tools/tidb-sql.md index db1f2821d4c4..e5cb857e94e0 100644 --- a/docs/en/integrations/tidb/tools/tidb-sql.md +++ b/docs/en/integrations/tidb/tools/tidb-sql.md @@ -72,7 +72,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -101,5 +101,5 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](..#specifying-parameters) | false | List of [parameters](..#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/trino/tools/trino-sql.md b/docs/en/integrations/trino/tools/trino-sql.md index 281d62554d9c..486b92ca0285 100644 --- a/docs/en/integrations/trino/tools/trino-sql.md +++ b/docs/en/integrations/trino/tools/trino-sql.md @@ -70,7 +70,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -99,5 +99,5 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/docs/en/integrations/yuagbytedb/tools/yugabytedb-sql.md b/docs/en/integrations/yuagbytedb/tools/yugabytedb-sql.md index c103fdd205f9..2140c025281f 100644 --- a/docs/en/integrations/yuagbytedb/tools/yugabytedb-sql.md +++ b/docs/en/integrations/yuagbytedb/tools/yugabytedb-sql.md @@ -73,7 +73,7 @@ parameters: > including identifiers, column names, and table names. **This makes it more > vulnerable to SQL injections**. Using basic parameters only (see above) is > recommended for performance and safety reasons. For more details, please check -> [templateParameters](..#template-parameters). +> [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters). ```yaml kind: tool @@ -102,5 +102,5 @@ templateParameters: | source | string | true | Name of the source the SQL should execute on. | | description | string | true | Description of the tool that is passed to the LLM. | | statement | string | true | SQL statement to execute on. | -| parameters | [parameters](..#specifying-parameters) | false | List of [parameters](..#specifying-parameters) that will be inserted into the SQL statement. | -| templateParameters | [templateParameters](..#template-parameters) | false | List of [templateParameters](..#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | +| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be inserted into the SQL statement. | +| templateParameters | [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) | false | List of [templateParameters](../../../documentation/configuration/tools/_index.md#template-parameters) that will be inserted into the SQL statement before executing prepared statement. | diff --git a/go.mod b/go.mod index 846c2cdfaa7e..7963884705b8 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,16 @@ toolchain go1.26.1 require ( cloud.google.com/go/alloydbconn v1.18.1 cloud.google.com/go/bigquery v1.75.0 - cloud.google.com/go/bigtable v1.44.0 + cloud.google.com/go/bigtable v1.45.0 cloud.google.com/go/cloudsqlconn v1.20.2 - cloud.google.com/go/dataplex v1.29.0 - cloud.google.com/go/dataproc/v2 v2.16.0 + cloud.google.com/go/dataplex v1.30.0 + cloud.google.com/go/dataproc/v2 v2.17.0 cloud.google.com/go/firestore v1.21.0 - cloud.google.com/go/geminidataanalytics v0.8.0 + cloud.google.com/go/geminidataanalytics v0.9.0 cloud.google.com/go/logging v1.13.2 - cloud.google.com/go/longrunning v0.8.0 + cloud.google.com/go/longrunning v0.9.0 cloud.google.com/go/spanner v1.89.0 - github.com/ClickHouse/clickhouse-go/v2 v2.43.0 + github.com/ClickHouse/clickhouse-go/v2 v2.44.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.31.0 github.com/MicahParks/jwkset v0.11.0 @@ -45,7 +45,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/looker-open-source/sdk-codegen/go v0.26.6 github.com/microsoft/go-mssqldb v1.9.8 - github.com/nakagami/firebirdsql v0.9.17 + github.com/nakagami/firebirdsql v0.9.18 github.com/neo4j/neo4j-go-driver/v6 v6.0.0 github.com/redis/go-redis/v9 v9.18.0 github.com/sijms/go-ora/v2 v2.9.0 @@ -69,12 +69,12 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 golang.org/x/oauth2 v0.36.0 - google.golang.org/api v0.273.0 - google.golang.org/genai v1.52.0 + google.golang.org/api v0.274.0 + google.golang.org/genai v1.52.1 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 - modernc.org/sqlite v1.48.0 + modernc.org/sqlite v1.48.1 ) require ( @@ -96,7 +96,7 @@ require ( cloud.google.com/go/auth v0.19.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/iam v1.6.0 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/trace v1.11.7 // indirect dario.cat/mergo v1.0.2 // indirect @@ -164,7 +164,7 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -184,7 +184,7 @@ require ( github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/googleapis/gax-go/v2 v2.20.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect diff --git a/go.sum b/go.sum index 91c9f68407d2..46f423e6a4da 100644 --- a/go.sum +++ b/go.sum @@ -13,28 +13,28 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.75.0 h1:gI4AgIhXNZ8hxvPDOp4hLGUnpNBjoBor6POSLcrdWkY= cloud.google.com/go/bigquery v1.75.0/go.mod h1:zNCHWok+hfTgKCwNqT+V7GH/YmFFgZqjzljKCZBJTWc= -cloud.google.com/go/bigtable v1.44.0 h1:EHn54xxAMfOtB5eGkMQnS0mFAqtP0om0Toasgm9MXRY= -cloud.google.com/go/bigtable v1.44.0/go.mod h1:Z5Op9LUTHgKPbHvPC4ijAZmPvmeGnx3N/lnnt/AwDKI= +cloud.google.com/go/bigtable v1.45.0 h1:OWl6kq6Ju8m4fZkV3bve7n9882W4HrlXPqtW5zT5ttA= +cloud.google.com/go/bigtable v1.45.0/go.mod h1:Ztklmotutn5zkAYzsn2w8ye8wvy+azwyGwYmujW5JHg= cloud.google.com/go/cloudsqlconn v1.20.2 h1:r1BFbgxKA7h0jY13pGk8wBueUeLhqF27e5Hyaxl8Ua8= cloud.google.com/go/cloudsqlconn v1.20.2/go.mod h1:cGBrxU+pKs1NppBkecFC+rKn9B5GnEdlz7XrHbuwn7E= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= -cloud.google.com/go/dataplex v1.29.0 h1:g1RsvpxELtGdVwmuOiktBM6BPfFy8TyNzmWvf+6yDgc= -cloud.google.com/go/dataplex v1.29.0/go.mod h1:32rAjJhxo1tY5KivJ33872X5ZqR6ZjlE5ng5Uz7+hH0= -cloud.google.com/go/dataproc/v2 v2.16.0 h1:0g2hnjlQ8SQTnNeu+Bqqa61QPssfSZF3t+9ldRmx+VQ= -cloud.google.com/go/dataproc/v2 v2.16.0/go.mod h1:HlzFg8k1SK+bJN3Zsy2z5g6OZS1D4DYiDUgJtF0gJnE= +cloud.google.com/go/dataplex v1.30.0 h1:VeGEANl3ywJ7txZ79BN1BlRluzfxxyv/CbOsh2u4cVQ= +cloud.google.com/go/dataplex v1.30.0/go.mod h1:GgV6b+1viq2nMtr+AUzKNUbaR+tKGxdhVaMN8TPPu0w= +cloud.google.com/go/dataproc/v2 v2.17.0 h1:jqH6LpQaMytLb7xW6zu6GoL9v/lYWcRXqXqndgT9mXQ= +cloud.google.com/go/dataproc/v2 v2.17.0/go.mod h1:lUY58QBxs6IIScAo9ZZKSOxx3imkHBxz6dow9f4fSRM= cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM= cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= -cloud.google.com/go/geminidataanalytics v0.8.0 h1:XBkqGtqm15bTz/iS9nGdvKCml8ZYJoqXyt44VE4h3IU= -cloud.google.com/go/geminidataanalytics v0.8.0/go.mod h1:+dZgr/DNFFWAdAvsrbYUmP3buIMjgx6x+6oDbodozD4= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/geminidataanalytics v0.9.0 h1:JeuMqZp4PlD9i0TLv+c2oTZe+UEatmF6oRC582TwxD8= +cloud.google.com/go/geminidataanalytics v0.9.0/go.mod h1:IUJTO6abmPj5LNZWUqohEEeHmTgLDlIBFwwRC6VJZgM= +cloud.google.com/go/iam v1.6.0 h1:JiSIcEi38dWBKhB3BtfKCW+dMvCZJEhBA2BsaGJgoxs= +cloud.google.com/go/iam v1.6.0/go.mod h1:ZS6zEy7QHmcNO18mjO2viYv/n+wOUkhJqGNkPPGueGU= cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/spanner v1.89.0 h1:r3h5Z5RA8JRPf3HCvA6ujNhREIMhPY+MrDL9mkY8jS0= @@ -75,8 +75,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= -github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE= -github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= @@ -269,8 +269,8 @@ github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AY github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-goquery/goquery v1.0.1 h1:kpchVA1LdOFWdRpkDPESVdlb1JQI6ixsJ5MiNUITO7U= github.com/go-goquery/goquery v1.0.1/go.mod h1:W5s8OWbqWf6lG0LkXWBeh7U1Y/X5XTI0Br65MHF8uJk= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -364,8 +364,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= -github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/googleapis/gax-go/v2 v2.20.0 h1:NIKVuLhDlIV74muWlsMM4CcQZqN6JJ20Qcxd9YMuYcs= +github.com/googleapis/gax-go/v2 v2.20.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= @@ -490,8 +490,8 @@ github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/nakagami/chacha20 v0.1.0 h1:2fbf5KeVUw7oRpAe6/A7DqvBJLYYu0ka5WstFbnkEVo= github.com/nakagami/chacha20 v0.1.0/go.mod h1:xpoujepNFA7MvYLvX5xKHzlOHimDrLI9Ll8zfOJ0l2E= -github.com/nakagami/firebirdsql v0.9.17 h1:GsQ6HTFKgzCx5n3bqQdlIorMnH/6JDI7za07iMXBVpE= -github.com/nakagami/firebirdsql v0.9.17/go.mod h1:l3bG682R481NkM9CMlXz7zGaO2VTWnX5oTRReb3SAA0= +github.com/nakagami/firebirdsql v0.9.18 h1:f/wh+RJqDMmaV83ftX/mEMqYCVXfgGTKS4IfvUqs0ts= +github.com/nakagami/firebirdsql v0.9.18/go.mod h1:l3bG682R481NkM9CMlXz7zGaO2VTWnX5oTRReb3SAA0= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/neo4j/neo4j-go-driver/v6 v6.0.0 h1:xVAi6YLOfzXUx+1Lc/F2dUhpbN76BfKleZbAlnDFRiA= @@ -815,12 +815,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.273.0 h1:r/Bcv36Xa/te1ugaN1kdJ5LoA5Wj/cL+a4gj6FiPBjQ= -google.golang.org/api v0.273.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= +google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA= +google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genai v1.52.0 h1:ekVIxWHtLUNbt+v0WWi4j3JT4yrHDEbysMcHQcaCQoI= -google.golang.org/genai v1.52.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.52.1 h1:dYoljKtLDXMiBdVaClSJ/ZPwZ7j1N0lGjMhwOKOQUlk= +google.golang.org/genai v1.52.1/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= @@ -889,8 +889,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4= -modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= +modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/auth/generic/generic.go b/internal/auth/generic/generic.go index c2bd75d7dee9..7385a6176493 100644 --- a/internal/auth/generic/generic.go +++ b/internal/auth/generic/generic.go @@ -28,6 +28,7 @@ import ( "github.com/MicahParks/keyfunc/v3" "github.com/golang-jwt/jwt/v5" "github.com/googleapis/mcp-toolbox/internal/auth" + "github.com/googleapis/mcp-toolbox/internal/util" ) const AuthServiceType string = "generic" @@ -52,10 +53,12 @@ func (cfg Config) AuthServiceConfigType() string { // Initialize a generic auth service func (cfg Config) Initialize() (auth.AuthService, error) { - // Discover the JWKS URL from the OIDC configuration endpoint - jwksURL, err := discoverJWKSURL(cfg.AuthorizationServer) + httpClient := newSecureHTTPClient() + + // Discover OIDC endpoints + jwksURL, introspectionURL, err := discoverOIDCConfig(httpClient, cfg.AuthorizationServer) if err != nil { - return nil, fmt.Errorf("failed to discover JWKS URL: %w", err) + return nil, fmt.Errorf("failed to discover OIDC config: %w", err) } // Create the keyfunc to fetch and cache the JWKS in the background @@ -65,28 +68,16 @@ func (cfg Config) Initialize() (auth.AuthService, error) { } a := &AuthService{ - Config: cfg, - kf: kf, + Config: cfg, + kf: kf, + client: httpClient, + introspectionURL: introspectionURL, } return a, nil } -func discoverJWKSURL(AuthorizationServer string) (string, error) { - u, err := url.Parse(AuthorizationServer) - if err != nil { - return "", fmt.Errorf("invalid auth URL") - } - if u.Scheme != "https" { - log.Printf("WARNING: HTTP instead of HTTPS is being used for AuthorizationServer: %s", AuthorizationServer) - } - - oidcConfigURL, err := url.JoinPath(AuthorizationServer, ".well-known/openid-configuration") - if err != nil { - return "", err - } - - // HTTP Client - client := &http.Client{ +func newSecureHTTPClient() *http.Client { + return &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ ForceAttemptHTTP2: true, @@ -95,49 +86,64 @@ func discoverJWKSURL(AuthorizationServer string) (string, error) { TLSHandshakeTimeout: 5 * time.Second, ExpectContinueTimeout: 1 * time.Second, }, - // Prevent redirect loops or redirects to internal sites CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } +} + +func discoverOIDCConfig(client *http.Client, AuthorizationServer string) (jwksURI string, introspectionEndpoint string, err error) { + u, err := url.Parse(AuthorizationServer) + if err != nil { + return "", "", fmt.Errorf("invalid auth URL") + } + if u.Scheme != "https" { + log.Printf("WARNING: HTTP instead of HTTPS is being used for AuthorizationServer: %s", AuthorizationServer) + } + + oidcConfigURL, err := url.JoinPath(AuthorizationServer, ".well-known/openid-configuration") + if err != nil { + return "", "", err + } resp, err := client.Get(oidcConfigURL) if err != nil { - return "", fmt.Errorf("failed to fetch OIDC config: %w", err) + return "", "", fmt.Errorf("failed to fetch OIDC config: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status: %d", resp.StatusCode) + return "", "", fmt.Errorf("unexpected status: %d", resp.StatusCode) } // Limit read size to 1MB to prevent memory exhaustion body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { - return "", err + return "", "", err } var config struct { - JWKSURI string `json:"jwks_uri"` + JwksUri string `json:"jwks_uri"` + IntrospectionEndpoint string `json:"introspection_endpoint"` } if err := json.Unmarshal(body, &config); err != nil { - return "", err + return "", "", err } - if config.JWKSURI == "" { - return "", fmt.Errorf("jwks_uri not found in config") + if config.JwksUri == "" { + return "", "", fmt.Errorf("jwks_uri not found in config") } // Sanitize the resulting JWKS URI before returning it - parsedJWKS, err := url.Parse(config.JWKSURI) + parsedJWKS, err := url.Parse(config.JwksUri) if err != nil { - return "", fmt.Errorf("invalid jwks_uri detected") + return "", "", fmt.Errorf("invalid jwks_uri detected") } if parsedJWKS.Scheme != "https" { - log.Printf("WARNING: HTTP instead of HTTPS is being used for JWKS URI: %s", config.JWKSURI) + log.Printf("WARNING: HTTP instead of HTTPS is being used for JWKS URI: %s", config.JwksUri) } - return config.JWKSURI, nil + return config.JwksUri, config.IntrospectionEndpoint, nil } var _ auth.AuthService = AuthService{} @@ -145,7 +151,9 @@ var _ auth.AuthService = AuthService{} // struct used to store auth service info type AuthService struct { Config - kf keyfunc.Keyfunc + kf keyfunc.Keyfunc + client *http.Client + introspectionURL string } // Returns the auth service type @@ -230,7 +238,21 @@ func (a AuthService) ValidateMCPAuth(ctx context.Context, h http.Header) error { return &MCPAuthError{Code: http.StatusUnauthorized, Message: "authorization header must be in the format 'Bearer '", ScopesRequired: a.ScopesRequired} } - token, err := jwt.Parse(headerParts[1], a.kf.Keyfunc) + tokenStr := headerParts[1] + + if isJWTFormat(tokenStr) { + return a.validateJwtToken(ctx, tokenStr) + } + return a.validateOpaqueToken(ctx, tokenStr) +} + +func isJWTFormat(token string) bool { + return strings.Count(token, ".") == 2 +} + +// validateJwtToken validates a JWT token locally +func (a AuthService) validateJwtToken(ctx context.Context, tokenStr string) error { + token, err := jwt.Parse(tokenStr, a.kf.Keyfunc) if err != nil || !token.Valid { return &MCPAuthError{Code: http.StatusUnauthorized, Message: "invalid or expired token", ScopesRequired: a.ScopesRequired} } @@ -246,26 +268,122 @@ func (a AuthService) ValidateMCPAuth(ctx context.Context, h http.Header) error { return &MCPAuthError{Code: http.StatusUnauthorized, Message: "could not parse audience from token", ScopesRequired: a.ScopesRequired} } - isAudValid := false - for _, audItem := range aud { - if audItem == a.Audience { - isAudValid = true - break + scopeClaim, _ := claims["scope"].(string) + + return a.validateClaims(ctx, aud, scopeClaim) +} + +// validateOpaqueToken validates an opaque token by calling the introspection endpoint +func (a AuthService) validateOpaqueToken(ctx context.Context, tokenStr string) error { + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return fmt.Errorf("failed to get logger from context: %w", err) + } + + introspectionURL := a.introspectionURL + if introspectionURL == "" { + introspectionURL, err = url.JoinPath(a.AuthorizationServer, "introspect") + if err != nil { + return fmt.Errorf("failed to construct introspection URL: %w", err) } } - if !isAudValid { - return &MCPAuthError{Code: http.StatusUnauthorized, Message: "audience validation failed", ScopesRequired: a.ScopesRequired} + data := url.Values{} + data.Set("token", tokenStr) + + req, err := http.NewRequestWithContext(ctx, "POST", introspectionURL, strings.NewReader(data.Encode())) + if err != nil { + return fmt.Errorf("failed to create introspection request: %w", err) } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") - // Check scopes - if len(a.ScopesRequired) > 0 { - scopeClaim, ok := claims["scope"].(string) - if !ok { - return &MCPAuthError{Code: http.StatusForbidden, Message: "insufficient scopes", ScopesRequired: a.ScopesRequired} + // Send request to auth server's introspection endpoint + resp, err := a.client.Do(req) + if err != nil { + logger.ErrorContext(ctx, "failed to call introspection endpoint: %v", err) + return &MCPAuthError{Code: http.StatusInternalServerError, Message: fmt.Sprintf("failed to call introspection endpoint: %v", err), ScopesRequired: a.ScopesRequired} + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.WarnContext(ctx, "introspection failed with status: %d", resp.StatusCode) + return &MCPAuthError{Code: http.StatusUnauthorized, Message: fmt.Sprintf("introspection failed with status: %d", resp.StatusCode), ScopesRequired: a.ScopesRequired} + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return fmt.Errorf("failed to read introspection response: %w", err) + } + + var introspectResp struct { + Active bool `json:"active"` + Scope string `json:"scope"` + Aud json.RawMessage `json:"aud"` + Exp int64 `json:"exp"` + } + + if err := json.Unmarshal(body, &introspectResp); err != nil { + return fmt.Errorf("failed to parse introspection response: %w", err) + } + + if !introspectResp.Active { + logger.InfoContext(ctx, "token is not active") + return &MCPAuthError{Code: http.StatusUnauthorized, Message: "token is not active", ScopesRequired: a.ScopesRequired} + } + + // Verify expiration (with 1 minute leeway) + const leeway = 60 + if introspectResp.Exp > 0 && time.Now().Unix() > (introspectResp.Exp+leeway) { + logger.WarnContext(ctx, "token has expired: exp=%d, now=%d", introspectResp.Exp, time.Now().Unix()) + return &MCPAuthError{Code: http.StatusUnauthorized, Message: "token has expired", ScopesRequired: a.ScopesRequired} + } + + // Extract audience + // According to RFC 7662, the aud claim can be a string or an array of strings + var aud []string + if len(introspectResp.Aud) > 0 { + var audStr string + var audArr []string + if err := json.Unmarshal(introspectResp.Aud, &audStr); err == nil { + aud = []string{audStr} + } else if err := json.Unmarshal(introspectResp.Aud, &audArr); err == nil { + aud = audArr + } else { + logger.WarnContext(ctx, "failed to parse aud claim in introspection response") + return &MCPAuthError{Code: http.StatusUnauthorized, Message: "invalid aud claim", ScopesRequired: a.ScopesRequired} + } + } + + return a.validateClaims(ctx, aud, introspectResp.Scope) +} + +// validateClaims validates the audience and scopes of a token +func (a AuthService) validateClaims(ctx context.Context, aud []string, scopeStr string) error { + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return fmt.Errorf("failed to get logger from context: %w", err) + } + + // Validate audience + if a.Audience != "" { + isAudValid := false + for _, audItem := range aud { + if audItem == a.Audience { + isAudValid = true + break + } } - tokenScopes := strings.Split(scopeClaim, " ") + if !isAudValid { + logger.WarnContext(ctx, "audience validation failed: expected %s", a.Audience) + return &MCPAuthError{Code: http.StatusUnauthorized, Message: "audience validation failed", ScopesRequired: a.ScopesRequired} + } + } + + // Check scopes + if len(a.ScopesRequired) > 0 { + tokenScopes := strings.Split(scopeStr, " ") scopeMap := make(map[string]bool) for _, s := range tokenScopes { scopeMap[s] = true @@ -273,6 +391,7 @@ func (a AuthService) ValidateMCPAuth(ctx context.Context, h http.Header) error { for _, requiredScope := range a.ScopesRequired { if !scopeMap[requiredScope] { + logger.WarnContext(ctx, "insufficient scopes: missing %s", requiredScope) return &MCPAuthError{Code: http.StatusForbidden, Message: "insufficient scopes", ScopesRequired: a.ScopesRequired} } } diff --git a/internal/auth/generic/generic_test.go b/internal/auth/generic/generic_test.go index 0238eb1b6c6e..d68b383409d5 100644 --- a/internal/auth/generic/generic_test.go +++ b/internal/auth/generic/generic_test.go @@ -15,6 +15,7 @@ package generic import ( + "bytes" "context" "crypto/rand" "crypto/rsa" @@ -27,6 +28,8 @@ import ( "github.com/MicahParks/jwkset" "github.com/golang-jwt/jwt/v5" + "github.com/googleapis/mcp-toolbox/internal/log" + "github.com/googleapis/mcp-toolbox/internal/util" ) func generateRSAPrivateKey(t *testing.T) *rsa.PrivateKey { @@ -206,3 +209,503 @@ func TestGetClaimsFromHeader(t *testing.T) { }) } } + +func TestValidateMCPAuth_Opaque(t *testing.T) { + tests := []struct { + name string + token string + scopesRequired []string + audience string + mockOidcConfig map[string]any + mockResponse map[string]any + mockStatus int + wantError bool + errContains string + }{ + { + name: "valid opaque token", + token: "opaque-valid", + scopesRequired: []string{"read:files"}, + audience: "my-audience", + mockResponse: map[string]any{ + "active": true, + "scope": "read:files write:files", + "aud": "my-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: false, + }, + { + name: "valid opaque token with custom introspection endpoint", + token: "opaque-valid-custom", + scopesRequired: []string{"read:files"}, + audience: "my-audience", + mockOidcConfig: map[string]any{ + "introspection_endpoint": "http://SERVER_HOST/custom-introspect", + }, + mockResponse: map[string]any{ + "active": true, + "scope": "read:files", + "aud": "my-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: false, + }, + { + name: "valid opaque token with array aud", + token: "opaque-valid-array-aud", + scopesRequired: []string{"read:files"}, + audience: "my-audience", + mockResponse: map[string]any{ + "active": true, + "scope": "read:files", + "aud": []string{"other-audience", "my-audience"}, + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: false, + }, + { + name: "inactive opaque token", + token: "opaque-inactive", + scopesRequired: []string{"read:files"}, + mockResponse: map[string]any{ + "active": false, + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "token is not active", + }, + { + name: "insufficient scopes", + token: "opaque-bad-scope", + scopesRequired: []string{"read:files", "write:files"}, + mockResponse: map[string]any{ + "active": true, + "scope": "read:files", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "insufficient scopes", + }, + { + name: "audience mismatch", + token: "opaque-bad-aud", + audience: "my-audience", + mockResponse: map[string]any{ + "active": true, + "aud": "wrong-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "audience validation failed", + }, + { + name: "expired token", + token: "opaque-expired", + mockResponse: map[string]any{ + "active": true, + "exp": time.Now().Add(-1 * time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "token has expired", + }, + { + name: "introspection error status", + token: "opaque-error", + mockResponse: map[string]any{ + "error": "server_error", + }, + mockStatus: http.StatusInternalServerError, + wantError: true, + errContains: "introspection failed with status: 500", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/.well-known/openid-configuration" { + w.Header().Set("Content-Type", "application/json") + config := map[string]interface{}{ + "issuer": "https://example.com", + "jwks_uri": "http://" + r.Host + "/jwks", + } + if tc.mockOidcConfig != nil { + for k, v := range tc.mockOidcConfig { + valStr, ok := v.(string) + if ok && strings.Contains(valStr, "SERVER_HOST") { + config[k] = strings.Replace(valStr, "SERVER_HOST", r.Host, 1) + } else { + config[k] = v + } + } + } + _ = json.NewEncoder(w).Encode(config) + return + } + if r.URL.Path == "/jwks" { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "keys": []any{}, + }) + return + } + if r.URL.Path == "/introspect" || r.URL.Path == "/custom-introspect" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tc.mockStatus) + _ = json.NewEncoder(w).Encode(tc.mockResponse) + return + } + http.NotFound(w, r) + }) + server := httptest.NewServer(handler) + defer server.Close() + + cfg := Config{ + Name: "test-generic-auth", + Type: "generic", + Audience: tc.audience, + AuthorizationServer: server.URL, + ScopesRequired: tc.scopesRequired, + } + + authService, err := cfg.Initialize() + if err != nil { + t.Fatalf("failed to initialize auth service: %v", err) + } + + genericAuth, ok := authService.(*AuthService) + if !ok { + t.Fatalf("expected *AuthService, got %T", authService) + } + + logger, err := log.NewLogger("standard", log.Debug, &bytes.Buffer{}, &bytes.Buffer{}) + if err != nil { + t.Fatalf("failed to create logger: %v", err) + } + ctx := util.WithLogger(context.Background(), logger) + + header := http.Header{} + header.Set("Authorization", "Bearer "+tc.token) + + err = genericAuth.ValidateMCPAuth(ctx, header) + + if tc.wantError { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) { + t.Errorf("expected error containing %q, got: %v", tc.errContains, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + }) + } +} + +func TestValidateJwtToken(t *testing.T) { + privateKey := generateRSAPrivateKey(t) + keyID := "test-key-id" + server := setupJWKSMockServer(t, privateKey, keyID) + defer server.Close() + + cfg := Config{ + Name: "test-generic-auth", + Type: "generic", + Audience: "my-audience", + AuthorizationServer: server.URL, + ScopesRequired: []string{"read:files"}, + } + + authService, err := cfg.Initialize() + if err != nil { + t.Fatalf("failed to initialize auth service: %v", err) + } + + genericAuth, ok := authService.(*AuthService) + if !ok { + t.Fatalf("expected *AuthService, got %T", authService) + } + + tests := []struct { + name string + token string + wantError bool + errContains string + }{ + { + name: "valid jwt", + token: generateValidToken(t, privateKey, keyID, jwt.MapClaims{ + "aud": "my-audience", + "scope": "read:files", + "exp": time.Now().Add(time.Hour).Unix(), + }), + wantError: false, + }, + { + name: "invalid token (wrong signature)", + token: "header.payload.signature", + wantError: true, + errContains: "invalid or expired token", + }, + { + name: "audience mismatch", + token: generateValidToken(t, privateKey, keyID, jwt.MapClaims{ + "aud": "wrong-audience", + "scope": "read:files", + "exp": time.Now().Add(time.Hour).Unix(), + }), + wantError: true, + errContains: "audience validation failed", + }, + { + name: "insufficient scopes", + token: generateValidToken(t, privateKey, keyID, jwt.MapClaims{ + "aud": "my-audience", + "scope": "wrong:scope", + "exp": time.Now().Add(time.Hour).Unix(), + }), + wantError: true, + errContains: "insufficient scopes", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger, err := log.NewLogger("standard", log.Debug, &bytes.Buffer{}, &bytes.Buffer{}) + if err != nil { + t.Fatalf("failed to create logger: %v", err) + } + ctx := util.WithLogger(context.Background(), logger) + err = genericAuth.validateJwtToken(ctx, tc.token) + if tc.wantError { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) { + t.Errorf("expected error containing %q, got: %v", tc.errContains, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + }) + } +} + +func TestValidateOpaqueToken(t *testing.T) { + tests := []struct { + name string + token string + scopesRequired []string + audience string + mockOidcConfig map[string]any + mockResponse map[string]any + mockStatus int + wantError bool + errContains string + }{ + { + name: "valid opaque token", + token: "opaque-valid", + scopesRequired: []string{"read:files"}, + audience: "my-audience", + mockResponse: map[string]any{ + "active": true, + "scope": "read:files write:files", + "aud": "my-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: false, + }, + { + name: "valid opaque token with custom introspection endpoint", + token: "opaque-valid-custom", + scopesRequired: []string{"read:files"}, + audience: "my-audience", + mockOidcConfig: map[string]any{ + "introspection_endpoint": "http://SERVER_HOST/custom-introspect", + }, + mockResponse: map[string]any{ + "active": true, + "scope": "read:files", + "aud": "my-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: false, + }, + { + name: "valid opaque token with array aud", + token: "opaque-valid-array-aud", + scopesRequired: []string{"read:files"}, + audience: "my-audience", + mockResponse: map[string]any{ + "active": true, + "scope": "read:files", + "aud": []string{"other-audience", "my-audience"}, + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: false, + }, + { + name: "inactive opaque token", + token: "opaque-inactive", + scopesRequired: []string{"read:files"}, + mockResponse: map[string]any{ + "active": false, + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "token is not active", + }, + { + name: "insufficient scopes", + token: "opaque-bad-scope", + scopesRequired: []string{"read:files", "write:files"}, + mockResponse: map[string]any{ + "active": true, + "scope": "read:files", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "insufficient scopes", + }, + { + name: "audience mismatch", + token: "opaque-bad-aud", + audience: "my-audience", + mockResponse: map[string]any{ + "active": true, + "aud": "wrong-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "audience validation failed", + }, + { + name: "expired token", + token: "opaque-expired", + mockResponse: map[string]any{ + "active": true, + "exp": time.Now().Add(-1 * time.Hour).Unix(), + }, + mockStatus: http.StatusOK, + wantError: true, + errContains: "token has expired", + }, + { + name: "introspection error status", + token: "opaque-error", + mockResponse: map[string]any{ + "error": "server_error", + }, + mockStatus: http.StatusInternalServerError, + wantError: true, + errContains: "introspection failed with status: 500", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/introspect" || r.URL.Path == "/custom-introspect" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tc.mockStatus) + _ = json.NewEncoder(w).Encode(tc.mockResponse) + return + } + http.NotFound(w, r) + }) + server := httptest.NewServer(handler) + defer server.Close() + + genericAuth := &AuthService{ + Config: Config{ + Audience: tc.audience, + AuthorizationServer: server.URL, + ScopesRequired: tc.scopesRequired, + }, + client: newSecureHTTPClient(), + } + + logger, err := log.NewLogger("standard", log.Debug, &bytes.Buffer{}, &bytes.Buffer{}) + if err != nil { + t.Fatalf("failed to create logger: %v", err) + } + ctx := util.WithLogger(context.Background(), logger) + + err = genericAuth.validateOpaqueToken(ctx, tc.token) + + if tc.wantError { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) { + t.Errorf("expected error containing %q, got: %v", tc.errContains, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + }) + } +} + +func TestIsJWTFormat(t *testing.T) { + tests := []struct { + name string + token string + want bool + }{ + { + name: "valid JWT format", + token: "header.payload.signature", + want: true, + }, + { + name: "opaque token", + token: "opaque-token", + want: false, + }, + { + name: "too many dots", + token: "a.b.c.d", + want: false, + }, + { + name: "too few dots", + token: "a.b", + want: false, + }, + { + name: "empty string", + token: "", + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := isJWTFormat(tc.token) + if got != tc.want { + t.Errorf("isJWTFormat(%q) = %v; want %v", tc.token, got, tc.want) + } + }) + } +} diff --git a/internal/prebuiltconfigs/tools/looker-dev.yaml b/internal/prebuiltconfigs/tools/looker-dev.yaml index 5a3b9ba6b0db..75eafda2440e 100644 --- a/internal/prebuiltconfigs/tools/looker-dev.yaml +++ b/internal/prebuiltconfigs/tools/looker-dev.yaml @@ -388,25 +388,55 @@ tools: Output: A confirmation message upon successful view generation, or an error message if the operation fails. - project_git_branch: - kind: looker-git-branch + list_git_branches: + kind: looker-list-git-branches source: looker-source description: | - This tool is used to retrieve and manipulate the git branch of a LookML project. + This tool is used to retrieve the list of available git branches of a LookML project. - An operation id must be provided which is one of the following: - * `list` - List all the available branch names. - * `get` - Get the current branch name. - * `create` - Create a new branch. The branch is initial set to the current ref, unless ref is specified. This only works in dev mode. - * `switch` - Change the branch to the given branch, and update to the given ref if specified. This only works in dev mode. - * `delete` - Delete a branch. This only works in dev mode. + Parameters: + - project_id (required): The unique ID of the LookML project. + + get_git_branch: + kind: looker-get-git-branch + source: looker-source + description: | + This tool is used to retrieve the current git branch of a LookML project. + + Parameters: + - project_id (required): The unique ID of the LookML project. + + create_git_branch: + kind: looker-create-git-branch + source: looker-source + description: | + This tool is used to create a new git branch of a LookML project. This only works in dev mode. Parameters: - project_id (required): The unique ID of the LookML project. - - operation (required): One of `list`, `get`, `create`, `switch`, or `delete`. - - branch (optional): The branch to create, switch to, or delete. - - ref (optional): The ref to start a newly created branch, or change a branch with `reset --hard` on a switch operation. + - branch (required): The branch to create. + - ref (optional): The ref to start a newly created branch. + switch_git_branch: + kind: looker-switch-git-branch + source: looker-source + description: | + This tool is used to switch the git branch of a LookML project. This only works in dev mode. + + Parameters: + - project_id (required): The unique ID of the LookML project. + - branch (required): The branch to switch to. + - ref (optional): The ref to change a branch with `reset --hard` on a switch operation. + + delete_git_branch: + kind: looker-delete-git-branch + source: looker-source + description: | + This tool is used to delete a git branch of a LookML project. This only works in dev mode. + + Parameters: + - project_id (required): The unique ID of the LookML project. + - branch (required): The branch to delete. toolsets: looker_dev_tools: - health_pulse @@ -431,4 +461,8 @@ toolsets: - get_lookml_tests - run_lookml_tests - create_view_from_table - - project_git_branch + - list_git_branches + - get_git_branch + - create_git_branch + - switch_git_branch + - delete_git_branch diff --git a/internal/sources/cloudsqlpg/cloud_sql_pg.go b/internal/sources/cloudsqlpg/cloud_sql_pg.go index 6ab289149cb6..818e2271cb29 100644 --- a/internal/sources/cloudsqlpg/cloud_sql_pg.go +++ b/internal/sources/cloudsqlpg/cloud_sql_pg.go @@ -71,9 +71,17 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So err = pool.Ping(ctx) if err != nil { + pool.Close() return nil, fmt.Errorf("unable to connect successfully: %w", err) } + var res int + err = pool.QueryRow(ctx, "SELECT 1").Scan(&res) + if err != nil { + pool.Close() + return nil, fmt.Errorf("failed to execute 'SELECT 1' after connection: %w", err) + } + s := &Source{ Config: r, Pool: pool, diff --git a/internal/tools/bigquery/bigquerysql/bigquerysql.go b/internal/tools/bigquery/bigquerysql/bigquerysql.go index e14170ee4e2e..58c8cd40c49a 100644 --- a/internal/tools/bigquery/bigquerysql/bigquerysql.go +++ b/internal/tools/bigquery/bigquerysql/bigquerysql.go @@ -19,6 +19,7 @@ import ( "fmt" "net/http" "reflect" + "strconv" "strings" bigqueryapi "cloud.google.com/go/bigquery" @@ -108,6 +109,7 @@ type Tool struct { func (t Tool) ToConfig() tools.ToolConfig { return t.Config } + func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) if err != nil { @@ -153,28 +155,44 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para Value: value, }) - // 2. Create the low-level parameter for the dry run, using the defined type from `p`. + // 2. Create the low-level parameter for the dry run. lowLevelParam := &bigqueryrestapi.QueryParameter{ Name: paramNameForHighLevel, ParameterType: &bigqueryrestapi.QueryParameterType{}, ParameterValue: &bigqueryrestapi.QueryParameterValue{}, } - if arrayParam, ok := p.(*parameters.ArrayParameter); ok { - // Handle array types based on their defined item type. + rv := reflect.ValueOf(value) + if rv.Kind() == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8 { lowLevelParam.ParameterType.Type = "ARRAY" - itemType, err := bqutil.BQTypeStringFromToolType(arrayParam.GetItems().GetType()) - if err != nil { - return nil, util.NewAgentError("unable to get BigQuery type from tool parameter type", err) + + // Default item type to FLOAT64 for embeddings, or use config if available. + itemType := "FLOAT64" + if arrayParam, ok := p.(*parameters.ArrayParameter); ok { + if bqType, err := bqutil.BQTypeStringFromToolType(arrayParam.GetItems().GetType()); err == nil { + itemType = bqType + } } lowLevelParam.ParameterType.ArrayType = &bigqueryrestapi.QueryParameterType{Type: itemType} // Build the array values. - sliceVal := reflect.ValueOf(value) - arrayValues := make([]*bigqueryrestapi.QueryParameterValue, sliceVal.Len()) - for i := 0; i < sliceVal.Len(); i++ { + arrayValues := make([]*bigqueryrestapi.QueryParameterValue, rv.Len()) + for i := 0; i < rv.Len(); i++ { + val := rv.Index(i).Interface() + + // Prevent precision loss and scientific notation issues + var valStr string + switch v := val.(type) { + case float64: + valStr = strconv.FormatFloat(v, 'f', -1, 64) + case float32: + valStr = strconv.FormatFloat(float64(v), 'f', -1, 32) + default: + valStr = fmt.Sprintf("%v", val) + } + arrayValues[i] = &bigqueryrestapi.QueryParameterValue{ - Value: fmt.Sprintf("%v", sliceVal.Index(i).Interface()), + Value: valStr, } } lowLevelParam.ParameterValue.ArrayValues = arrayValues @@ -220,8 +238,21 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para return resp, nil } +func formatVectorForBigQuery(vectorFloats []float32) any { + if len(vectorFloats) == 0 { + return []float64{} + } + + res := make([]float64, len(vectorFloats)) + for i, f := range vectorFloats { + // Convert to float64 + res[i] = float64(f) + } + return res +} + func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { - return parameters.EmbedParams(ctx, t.AllParams, paramValues, embeddingModelsMap, nil) + return parameters.EmbedParams(ctx, t.AllParams, paramValues, embeddingModelsMap, formatVectorForBigQuery) } func (t Tool) Manifest() tools.Manifest { diff --git a/internal/tools/elasticsearch/elasticsearchexecuteesql/elasticsearchexecuteesql.go b/internal/tools/elasticsearch/elasticsearchexecuteesql/elasticsearchexecuteesql.go new file mode 100644 index 000000000000..4e920603eb99 --- /dev/null +++ b/internal/tools/elasticsearch/elasticsearchexecuteesql/elasticsearchexecuteesql.go @@ -0,0 +1,155 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package elasticsearchexecuteesql + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + es "github.com/googleapis/mcp-toolbox/internal/sources/elasticsearch" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" +) + +const resourceType string = "elasticsearch-execute-esql" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + ElasticsearchClient() es.EsClient + RunSQL(ctx context.Context, format, query string, params []map[string]any) (any, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Format string `yaml:"format"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + queryParameter := parameters.NewStringParameter("query", "The ES|QL statement to execute.") + params := parameters.Parameters{queryParameter} + + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + t := Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{Description: cfg.Description, Parameters: params.Manifest(), AuthRequired: cfg.AuthRequired}, + mcpManifest: mcpManifest, + } + return t, nil +} + +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + paramsMap := params.AsMap() + query, ok := paramsMap["query"].(string) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("unable to get cast %s", paramsMap["query"]), nil) + } + + // Default to json format + format := t.Format + if format == "" { + format = "json" + } + + // Get logger + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError("error getting logger", http.StatusInternalServerError, err) + } + logger.DebugContext(ctx, fmt.Sprintf("executing `%s` tool query: %s with format: %s", resourceType, query, format)) + + resp, err := source.RunSQL(ctx, format, query, nil) + if err != nil { + return nil, util.ProcessGeneralError(err) + } + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + return false, nil +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + return "Authorization", nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/elasticsearch/elasticsearchexecuteesql/elasticsearchexecuteesql_test.go b/internal/tools/elasticsearch/elasticsearchexecuteesql/elasticsearchexecuteesql_test.go new file mode 100644 index 000000000000..03e7de895380 --- /dev/null +++ b/internal/tools/elasticsearch/elasticsearchexecuteesql/elasticsearchexecuteesql_test.go @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package elasticsearchexecuteesql + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" +) + +func TestParseFromYamlElasticsearchExecuteEsql(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic execute tool example", + in: ` + kind: tool + name: example_tool + type: elasticsearch-execute-esql + source: my-elasticsearch-instance + description: Elasticsearch execute ES|QL tool + `, + want: server.ToolConfigs{ + "example_tool": Config{ + Name: "example_tool", + Type: "elasticsearch-execute-esql", + Source: "my-elasticsearch-instance", + Description: "Elasticsearch execute ES|QL tool", + AuthRequired: []string{}, + }, + }, + }, + { + desc: "execute tool with format", + in: ` + kind: tool + name: example_tool_csv + type: elasticsearch-execute-esql + source: my-elasticsearch-instance + description: Elasticsearch execute ES|QL tool in CSV + format: csv + `, + want: server.ToolConfigs{ + "example_tool_csv": Config{ + Name: "example_tool_csv", + Type: "elasticsearch-execute-esql", + Source: "my-elasticsearch-instance", + Description: "Elasticsearch execute ES|QL tool in CSV", + AuthRequired: []string{}, + Format: "csv", + }, + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} diff --git a/internal/tools/looker/lookergitbranch/lookergitbranch.go b/internal/tools/looker/lookercreategitbranch/lookercreategitbranch.go similarity index 60% rename from internal/tools/looker/lookergitbranch/lookergitbranch.go rename to internal/tools/looker/lookercreategitbranch/lookercreategitbranch.go index bdb07bb1a5d1..0d6dad10805d 100644 --- a/internal/tools/looker/lookergitbranch/lookergitbranch.go +++ b/internal/tools/looker/lookercreategitbranch/lookercreategitbranch.go @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package lookergitbranch +package lookercreategitbranch import ( "context" @@ -29,7 +29,7 @@ import ( v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" ) -const resourceType string = "looker-git-branch" +const resourceType string = "looker-create-git-branch" func init() { if !tools.Register(resourceType, newConfig) { @@ -70,12 +70,16 @@ func (cfg Config) ToolConfigType() string { func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { projectIdParameter := parameters.NewStringParameter("project_id", "The project_id") - operationParameter := parameters.NewStringParameter("operation", "The operation, one of `list`, `get`, `create`, `switch`, or `delete`") - branchParameter := parameters.NewStringParameterWithDefault("branch", "", "The git branch on which to operate. Not required for `list` or `get` operations.") - refParameter := parameters.NewStringParameterWithDefault("ref", "", "The ref to use as the start of a new branch. If not specified for a `create` operation it will default to HEAD of current branch. If supplied with a `switch` operation will `reset --hard` the branch.") - params := parameters.Parameters{projectIdParameter, operationParameter, branchParameter, refParameter} + branchParameter := parameters.NewStringParameter("branch", "The git branch to create") + refParameter := parameters.NewStringParameterWithDefault("ref", "", "The ref to use as the start of a new branch. Defaults to HEAD of current branch if not specified.") + params := parameters.Parameters{projectIdParameter, branchParameter, refParameter} annotations := cfg.Annotations + if annotations == nil { + annotations = &tools.ToolAnnotations{} + } + readOnlyHint := false + annotations.ReadOnlyHint = &readOnlyHint mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) @@ -112,78 +116,30 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) } - logger, err := util.LoggerFromContext(ctx) - if err != nil { - return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err) - } - sdk, err := source.GetLookerSDK(string(accessToken)) if err != nil { return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) } mapParams := params.AsMap() - logger.DebugContext(ctx, "looker_git_branch params = ", mapParams) projectId := mapParams["project_id"].(string) - operation := mapParams["operation"].(string) branch := mapParams["branch"].(string) ref := mapParams["ref"].(string) + if branch == "" { + return nil, util.NewClientServerError("branch must be specified", http.StatusInternalServerError, nil) + } - switch operation { - case "list": - resp, err := sdk.AllGitBranches(projectId, source.LookerApiSettings()) - if err != nil { - return nil, util.NewClientServerError(fmt.Sprintf("error making list_git_branches request: %s", err), http.StatusInternalServerError, err) - } - return resp, nil - case "get": - resp, err := sdk.GitBranch(projectId, source.LookerApiSettings()) - if err != nil { - return nil, util.NewClientServerError(fmt.Sprintf("error making get_git_branch request: %s", err), http.StatusInternalServerError, err) - } - return resp, nil - case "create": - if branch == "" { - return nil, util.NewClientServerError(fmt.Sprintf("%s operation: branch must be specified", operation), http.StatusInternalServerError, nil) - } - body := v4.WriteGitBranch{ - Name: &branch, - } - if ref != "" { - body.Ref = &ref - } - resp, err := sdk.CreateGitBranch(projectId, body, source.LookerApiSettings()) - if err != nil { - return nil, util.NewClientServerError(fmt.Sprintf("error making create_git_branch request: %s", err), http.StatusInternalServerError, err) - } - return resp, nil - case "switch": - if branch == "" { - return nil, util.NewClientServerError(fmt.Sprintf("%s operation: branch must be specified", operation), http.StatusInternalServerError, nil) - } - body := v4.WriteGitBranch{ - Name: &branch, - } - if ref != "" { - body.Ref = &ref - } - resp, err := sdk.UpdateGitBranch(projectId, body, source.LookerApiSettings()) - if err != nil { - return nil, util.NewClientServerError(fmt.Sprintf("error making update_git_branch request: %s", err), http.StatusInternalServerError, err) - } - return resp, nil - case "delete": - if branch == "" { - return nil, util.NewClientServerError(fmt.Sprintf("%s operation: branch must be specified", operation), http.StatusInternalServerError, nil) - } - _, err := sdk.DeleteGitBranch(projectId, branch, source.LookerApiSettings()) - if err != nil { - return nil, util.NewClientServerError(fmt.Sprintf("error making delete_git_branch request: %s", err), http.StatusInternalServerError, err) - } - return fmt.Sprintf("Deleted branch %s", branch), nil - default: - return nil, util.NewClientServerError(fmt.Sprintf("unknown operation: %s. Must be one of `list`, `get`, `create`, `switch`, or `delete`", operation), http.StatusInternalServerError, nil) + body := v4.WriteGitBranch{ + Name: &branch, + } + if ref != "" { + body.Ref = &ref + } + resp, err := sdk.CreateGitBranch(projectId, body, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error making create_git_branch request: %s", err), http.StatusInternalServerError, err) } + return resp, nil } func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { diff --git a/internal/tools/looker/lookergitbranch/lookergitbranch_test.go b/internal/tools/looker/lookercreategitbranch/lookercreategitbranch_test.go similarity index 82% rename from internal/tools/looker/lookergitbranch/lookergitbranch_test.go rename to internal/tools/looker/lookercreategitbranch/lookercreategitbranch_test.go index f9ac2a0a83f1..55aed41ed6c6 100644 --- a/internal/tools/looker/lookergitbranch/lookergitbranch_test.go +++ b/internal/tools/looker/lookercreategitbranch/lookercreategitbranch_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package lookergitbranch_test +package lookercreategitbranch_test import ( "strings" @@ -21,10 +21,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/googleapis/mcp-toolbox/internal/server" "github.com/googleapis/mcp-toolbox/internal/testutils" - lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergitbranch" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreategitbranch" ) -func TestParseFromYamlLookerGitBranch(t *testing.T) { +func TestParseFromYamlLookerCreateGitBranch(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) @@ -39,14 +39,14 @@ func TestParseFromYamlLookerGitBranch(t *testing.T) { in: ` kind: tool name: example_tool - type: looker-git-branch + type: looker-create-git-branch source: my-instance description: some description `, want: server.ToolConfigs{ "example_tool": lkr.Config{ Name: "example_tool", - Type: "looker-git-branch", + Type: "looker-create-git-branch", Source: "my-instance", Description: "some description", AuthRequired: []string{}, @@ -56,7 +56,6 @@ func TestParseFromYamlLookerGitBranch(t *testing.T) { } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { - // Parse contents _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) if err != nil { t.Fatalf("unable to unmarshal: %s", err) @@ -66,10 +65,9 @@ func TestParseFromYamlLookerGitBranch(t *testing.T) { } }) } - } -func TestFailParseFromYamlLookerGitBranch(t *testing.T) { +func TestFailParseFromYamlLookerCreateGitBranch(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) @@ -84,17 +82,16 @@ func TestFailParseFromYamlLookerGitBranch(t *testing.T) { in: ` kind: tool name: example_tool - type: looker-git-branch + type: looker-create-git-branch source: my-instance method: GOT description: some description `, - err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-git-branch\": [3:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n> 3 | method: GOT\n ^\n 4 | name: example_tool\n 5 | source: my-instance\n 6 | type: looker-git-branch", + err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-create-git-branch\": [3:1] unknown field \"method\"", }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { - // Parse contents _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) if err == nil { t.Fatalf("expect parsing to fail") @@ -105,5 +102,4 @@ func TestFailParseFromYamlLookerGitBranch(t *testing.T) { } }) } - } diff --git a/internal/tools/looker/lookerdeletegitbranch/lookerdeletegitbranch.go b/internal/tools/looker/lookerdeletegitbranch/lookerdeletegitbranch.go new file mode 100644 index 000000000000..c15a84b5bb2f --- /dev/null +++ b/internal/tools/looker/lookerdeletegitbranch/lookerdeletegitbranch.go @@ -0,0 +1,173 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lookerdeletegitbranch + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-delete-git-branch" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The project_id") + branchParameter := parameters.NewStringParameter("branch", "The git branch to delete") + params := parameters.Parameters{projectIdParameter, branchParameter} + + annotations := cfg.Annotations + if annotations == nil { + annotations = &tools.ToolAnnotations{} + } + readOnlyHint := false + annotations.ReadOnlyHint = &readOnlyHint + destructiveHint := true + annotations.DestructiveHint = &destructiveHint + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId := mapParams["project_id"].(string) + branch := mapParams["branch"].(string) + if branch == "" { + return nil, util.NewClientServerError("branch must be specified", http.StatusInternalServerError, nil) + } + + _, err = sdk.DeleteGitBranch(projectId, branch, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error making delete_git_branch request: %s", err), http.StatusInternalServerError, err) + } + return fmt.Sprintf("Deleted branch %s", branch), nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerdeletegitbranch/lookerdeletegitbranch_test.go b/internal/tools/looker/lookerdeletegitbranch/lookerdeletegitbranch_test.go new file mode 100644 index 000000000000..5d05945a8ba7 --- /dev/null +++ b/internal/tools/looker/lookerdeletegitbranch/lookerdeletegitbranch_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookerdeletegitbranch_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeletegitbranch" +) + +func TestParseFromYamlLookerDeleteGitBranch(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tool + name: example_tool + type: looker-delete-git-branch + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-delete-git-branch", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} + +func TestFailParseFromYamlLookerDeleteGitBranch(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tool + name: example_tool + type: looker-delete-git-branch + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-delete-git-branch\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } +} diff --git a/internal/tools/looker/lookergetgitbranch/lookergetgitbranch.go b/internal/tools/looker/lookergetgitbranch/lookergetgitbranch.go new file mode 100644 index 000000000000..15c1457fdc0c --- /dev/null +++ b/internal/tools/looker/lookergetgitbranch/lookergetgitbranch.go @@ -0,0 +1,167 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lookergetgitbranch + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-get-git-branch" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The project_id") + params := parameters.Parameters{projectIdParameter} + + annotations := cfg.Annotations + if annotations == nil { + readOnlyHint := true + annotations = &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId := mapParams["project_id"].(string) + + resp, err := sdk.GitBranch(projectId, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error making get_git_branch request: %s", err), http.StatusInternalServerError, err) + } + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookergetgitbranch/lookergetgitbranch_test.go b/internal/tools/looker/lookergetgitbranch/lookergetgitbranch_test.go new file mode 100644 index 000000000000..4831b5a71a7b --- /dev/null +++ b/internal/tools/looker/lookergetgitbranch/lookergetgitbranch_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookergetgitbranch_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetgitbranch" +) + +func TestParseFromYamlLookerGetGitBranch(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tool + name: example_tool + type: looker-get-git-branch + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-get-git-branch", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} + +func TestFailParseFromYamlLookerGetGitBranch(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tool + name: example_tool + type: looker-get-git-branch + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-get-git-branch\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } +} diff --git a/internal/tools/looker/lookerlistgitbranches/lookerlistgitbranches.go b/internal/tools/looker/lookerlistgitbranches/lookerlistgitbranches.go new file mode 100644 index 000000000000..dec0ecbbc731 --- /dev/null +++ b/internal/tools/looker/lookerlistgitbranches/lookerlistgitbranches.go @@ -0,0 +1,167 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lookerlistgitbranches + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-list-git-branches" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The project_id") + params := parameters.Parameters{projectIdParameter} + + annotations := cfg.Annotations + if annotations == nil { + readOnlyHint := true + annotations = &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId := mapParams["project_id"].(string) + + resp, err := sdk.AllGitBranches(projectId, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error making list_git_branches request: %s", err), http.StatusInternalServerError, err) + } + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerlistgitbranches/lookerlistgitbranches_test.go b/internal/tools/looker/lookerlistgitbranches/lookerlistgitbranches_test.go new file mode 100644 index 000000000000..eb12b3a7caf0 --- /dev/null +++ b/internal/tools/looker/lookerlistgitbranches/lookerlistgitbranches_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookerlistgitbranches_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistgitbranches" +) + +func TestParseFromYamlLookerListGitBranches(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tool + name: example_tool + type: looker-list-git-branches + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-list-git-branches", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} + +func TestFailParseFromYamlLookerListGitBranches(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tool + name: example_tool + type: looker-list-git-branches + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-list-git-branches\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } +} diff --git a/internal/tools/looker/lookerswitchgitbranch/lookerswitchgitbranch.go b/internal/tools/looker/lookerswitchgitbranch/lookerswitchgitbranch.go new file mode 100644 index 000000000000..f6cee296edaf --- /dev/null +++ b/internal/tools/looker/lookerswitchgitbranch/lookerswitchgitbranch.go @@ -0,0 +1,181 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lookerswitchgitbranch + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-switch-git-branch" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The project_id") + branchParameter := parameters.NewStringParameter("branch", "The git branch to switch to") + refParameter := parameters.NewStringParameterWithDefault("ref", "", "The ref to switch the branch to using `reset --hard`.") + params := parameters.Parameters{projectIdParameter, branchParameter, refParameter} + + annotations := cfg.Annotations + if annotations == nil { + annotations = &tools.ToolAnnotations{} + } + readOnlyHint := false + annotations.ReadOnlyHint = &readOnlyHint + destructiveHint := true + annotations.DestructiveHint = &destructiveHint + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId := mapParams["project_id"].(string) + branch := mapParams["branch"].(string) + ref := mapParams["ref"].(string) + if branch == "" { + return nil, util.NewClientServerError("branch must be specified", http.StatusInternalServerError, nil) + } + + body := v4.WriteGitBranch{ + Name: &branch, + } + if ref != "" { + body.Ref = &ref + } + resp, err := sdk.UpdateGitBranch(projectId, body, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error making update_git_branch request: %s", err), http.StatusInternalServerError, err) + } + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerswitchgitbranch/lookerswitchgitbranch_test.go b/internal/tools/looker/lookerswitchgitbranch/lookerswitchgitbranch_test.go new file mode 100644 index 000000000000..3ad91d309409 --- /dev/null +++ b/internal/tools/looker/lookerswitchgitbranch/lookerswitchgitbranch_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookerswitchgitbranch_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerswitchgitbranch" +) + +func TestParseFromYamlLookerSwitchGitBranch(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tool + name: example_tool + type: looker-switch-git-branch + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-switch-git-branch", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} + +func TestFailParseFromYamlLookerSwitchGitBranch(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tool + name: example_tool + type: looker-switch-git-branch + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-switch-git-branch\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } +} diff --git a/tests/alloydb/alloydb_integration_test.go b/tests/alloydb/alloydb_integration_test.go index 5ff270bad531..27c11eb2fbc7 100644 --- a/tests/alloydb/alloydb_integration_test.go +++ b/tests/alloydb/alloydb_integration_test.go @@ -23,7 +23,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "os" "reflect" "regexp" "sort" @@ -37,115 +36,6 @@ import ( "github.com/googleapis/mcp-toolbox/tests" ) -var ( - AlloyDBProject = os.Getenv("ALLOYDB_PROJECT") - AlloyDBLocation = os.Getenv("ALLOYDB_REGION") - AlloyDBCluster = os.Getenv("ALLOYDB_CLUSTER") - AlloyDBInstance = os.Getenv("ALLOYDB_INSTANCE") - AlloyDBUser = os.Getenv("ALLOYDB_POSTGRES_USER") -) - -func getAlloyDBVars(t *testing.T) map[string]string { - if AlloyDBProject == "" { - t.Fatal("'ALLOYDB_PROJECT' not set") - } - if AlloyDBLocation == "" { - t.Fatal("'ALLOYDB_REGION' not set") - } - if AlloyDBCluster == "" { - t.Fatal("'ALLOYDB_CLUSTER' not set") - } - if AlloyDBInstance == "" { - t.Fatal("'ALLOYDB_INSTANCE' not set") - } - if AlloyDBUser == "" { - t.Fatal("'ALLOYDB_USER' not set") - } - return map[string]string{ - "project": AlloyDBProject, - "location": AlloyDBLocation, - "cluster": AlloyDBCluster, - "instance": AlloyDBInstance, - "user": AlloyDBUser, - } -} - -func getAlloyDBToolsConfig() map[string]any { - return map[string]any{ - "sources": map[string]any{ - "alloydb-admin-source": map[string]any{ - "type": "alloydb-admin", - }, - }, - "tools": map[string]any{ - // Tool for RunAlloyDBToolGetTest - "my-simple-tool": map[string]any{ - "type": "alloydb-list-clusters", - "source": "alloydb-admin-source", - "description": "Simple tool to test end to end functionality.", - }, - // Tool for MCP test - "my-param-tool": map[string]any{ - "type": "alloydb-list-clusters", - "source": "alloydb-admin-source", - "description": "Tool to list clusters", - }, - // Tool for MCP test that fails - "my-fail-tool": map[string]any{ - "type": "alloydb-list-clusters", - "source": "alloydb-admin-source", - "description": "Tool that will fail", - }, - // AlloyDB specific tools - "alloydb-list-clusters": map[string]any{ - "type": "alloydb-list-clusters", - "source": "alloydb-admin-source", - "description": "Lists all AlloyDB clusters in a given project and location.", - }, - "alloydb-list-users": map[string]any{ - "type": "alloydb-list-users", - "source": "alloydb-admin-source", - "description": "Lists all AlloyDB users within a specific cluster.", - }, - "alloydb-list-instances": map[string]any{ - "type": "alloydb-list-instances", - "source": "alloydb-admin-source", - "description": "Lists all AlloyDB instances within a specific cluster.", - }, - "alloydb-get-cluster": map[string]any{ - "type": "alloydb-get-cluster", - "source": "alloydb-admin-source", - "description": "Retrieves details of a specific AlloyDB cluster.", - }, - "alloydb-get-instance": map[string]any{ - "type": "alloydb-get-instance", - "source": "alloydb-admin-source", - "description": "Retrieves details of a specific AlloyDB instance.", - }, - "alloydb-get-user": map[string]any{ - "type": "alloydb-get-user", - "source": "alloydb-admin-source", - "description": "Retrieves details of a specific AlloyDB user.", - }, - "alloydb-create-cluster": map[string]any{ - "type": "alloydb-create-cluster", - "description": "create cluster", - "source": "alloydb-admin-source", - }, - "alloydb-create-instance": map[string]any{ - "type": "alloydb-create-instance", - "description": "create instance", - "source": "alloydb-admin-source", - }, - "alloydb-create-user": map[string]any{ - "type": "alloydb-create-user", - "description": "create user", - "source": "alloydb-admin-source", - }, - }, - } -} - func TestAlloyDBToolEndpoints(t *testing.T) { vars := getAlloyDBVars(t) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) diff --git a/tests/alloydb/alloydb_mcp_test.go b/tests/alloydb/alloydb_mcp_test.go index 200a61981672..9e195e0e5141 100644 --- a/tests/alloydb/alloydb_mcp_test.go +++ b/tests/alloydb/alloydb_mcp_test.go @@ -21,6 +21,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "reflect" "regexp" "sort" @@ -33,11 +34,138 @@ import ( "github.com/googleapis/mcp-toolbox/tests" ) +var ( + AlloyDBProject = os.Getenv("ALLOYDB_PROJECT") + AlloyDBLocation = os.Getenv("ALLOYDB_REGION") + AlloyDBCluster = os.Getenv("ALLOYDB_CLUSTER") + AlloyDBInstance = os.Getenv("ALLOYDB_INSTANCE") + AlloyDBUser = os.Getenv("ALLOYDB_POSTGRES_USER") +) + +func getAlloyDBVars(t *testing.T) map[string]string { + if AlloyDBProject == "" { + t.Fatal("'ALLOYDB_PROJECT' not set") + } + if AlloyDBLocation == "" { + t.Fatal("'ALLOYDB_REGION' not set") + } + if AlloyDBCluster == "" { + t.Fatal("'ALLOYDB_CLUSTER' not set") + } + if AlloyDBInstance == "" { + t.Fatal("'ALLOYDB_INSTANCE' not set") + } + if AlloyDBUser == "" { + t.Fatal("'ALLOYDB_USER' not set") + } + return map[string]string{ + "project": AlloyDBProject, + "location": AlloyDBLocation, + "cluster": AlloyDBCluster, + "instance": AlloyDBInstance, + "user": AlloyDBUser, + } +} + +func getAlloyDBToolsConfig() map[string]any { + return map[string]any{ + "sources": map[string]any{ + "alloydb-admin-source": map[string]any{ + "type": "alloydb-admin", + }, + }, + "tools": map[string]any{ + // Tool for RunAlloyDBToolGetTest + "my-simple-tool": map[string]any{ + "type": "alloydb-list-clusters", + "source": "alloydb-admin-source", + "description": "Simple tool to test end to end functionality.", + }, + // Tool for MCP test + "my-param-tool": map[string]any{ + "type": "alloydb-list-clusters", + "source": "alloydb-admin-source", + "description": "Tool to list clusters", + }, + // Tool for MCP test that fails + "my-fail-tool": map[string]any{ + "type": "alloydb-list-clusters", + "source": "alloydb-admin-source", + "description": "Tool that will fail", + }, + // AlloyDB specific tools + "alloydb-list-clusters": map[string]any{ + "type": "alloydb-list-clusters", + "source": "alloydb-admin-source", + "description": "Lists all AlloyDB clusters in a given project and location.", + }, + "alloydb-list-users": map[string]any{ + "type": "alloydb-list-users", + "source": "alloydb-admin-source", + "description": "Lists all AlloyDB users within a specific cluster.", + }, + "alloydb-list-instances": map[string]any{ + "type": "alloydb-list-instances", + "source": "alloydb-admin-source", + "description": "Lists all AlloyDB instances within a specific cluster.", + }, + "alloydb-get-cluster": map[string]any{ + "type": "alloydb-get-cluster", + "source": "alloydb-admin-source", + "description": "Retrieves details of a specific AlloyDB cluster.", + }, + "alloydb-get-instance": map[string]any{ + "type": "alloydb-get-instance", + "source": "alloydb-admin-source", + "description": "Retrieves details of a specific AlloyDB instance.", + }, + "alloydb-get-user": map[string]any{ + "type": "alloydb-get-user", + "source": "alloydb-admin-source", + "description": "Retrieves details of a specific AlloyDB user.", + }, + "alloydb-create-cluster": map[string]any{ + "type": "alloydb-create-cluster", + "description": "create cluster", + "source": "alloydb-admin-source", + }, + "alloydb-create-instance": map[string]any{ + "type": "alloydb-create-instance", + "description": "create instance", + "source": "alloydb-admin-source", + }, + "alloydb-create-user": map[string]any{ + "type": "alloydb-create-user", + "description": "create user", + "source": "alloydb-admin-source", + }, + }, + } +} + func TestAlloyDBListTools(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - toolsFile := getAlloyDBToolsConfig() + toolsFile := map[string]any{ + "sources": map[string]any{ + "alloydb-admin-source": map[string]any{ + "type": "alloydb-admin", + }, + }, + "tools": map[string]any{ + "alloydb-list-clusters": map[string]any{ + "type": "alloydb-list-clusters", + "source": "alloydb-admin-source", + "description": "Lists all AlloyDB clusters in a given project and location.", + }, + "alloydb-list-users": map[string]any{ + "type": "alloydb-list-users", + "source": "alloydb-admin-source", + "description": "Lists all AlloyDB users within a specific cluster.", + }, + }, + } // Start the toolbox server cmd, cleanup, err := tests.StartCmd(ctx, toolsFile) @@ -845,9 +973,8 @@ func TestAlloyDBCreateClusterMCP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - args := []string{"--enable-api"} toolsFile := getAlloyDBToolsConfig() - cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...) + cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } @@ -942,9 +1069,8 @@ func TestAlloyDBCreateInstanceMCP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - args := []string{"--enable-api"} toolsFile := getAlloyDBToolsConfig() - cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...) + cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } @@ -1049,9 +1175,8 @@ func TestAlloyDBCreateUserMCP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - args := []string{"--enable-api"} toolsFile := getAlloyDBToolsConfig() - cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...) + cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } diff --git a/tests/alloydbainl/alloydb_ai_nl_integration_test.go b/tests/alloydbainl/alloydb_ai_nl_integration_test.go index 42fc0e09c193..96a375f1181a 100644 --- a/tests/alloydbainl/alloydb_ai_nl_integration_test.go +++ b/tests/alloydbainl/alloydb_ai_nl_integration_test.go @@ -20,7 +20,6 @@ import ( "encoding/json" "io" "net/http" - "os" "reflect" "regexp" "strings" @@ -32,47 +31,6 @@ import ( "github.com/googleapis/mcp-toolbox/tests" ) -var ( - AlloyDBAINLSourceType = "alloydb-postgres" - AlloyDBAINLToolType = "alloydb-ai-nl" - AlloyDBAINLProject = os.Getenv("ALLOYDB_AI_NL_PROJECT") - AlloyDBAINLRegion = os.Getenv("ALLOYDB_AI_NL_REGION") - AlloyDBAINLCluster = os.Getenv("ALLOYDB_AI_NL_CLUSTER") - AlloyDBAINLInstance = os.Getenv("ALLOYDB_AI_NL_INSTANCE") - AlloyDBAINLDatabase = os.Getenv("ALLOYDB_AI_NL_DATABASE") - AlloyDBAINLUser = os.Getenv("ALLOYDB_AI_NL_USER") - AlloyDBAINLPass = os.Getenv("ALLOYDB_AI_NL_PASS") -) - -func getAlloyDBAINLVars(t *testing.T) map[string]any { - switch "" { - case AlloyDBAINLProject: - t.Fatal("'ALLOYDB_AI_NL_PROJECT' not set") - case AlloyDBAINLRegion: - t.Fatal("'ALLOYDB_AI_NL_REGION' not set") - case AlloyDBAINLCluster: - t.Fatal("'ALLOYDB_AI_NL_CLUSTER' not set") - case AlloyDBAINLInstance: - t.Fatal("'ALLOYDB_AI_NL_INSTANCE' not set") - case AlloyDBAINLDatabase: - t.Fatal("'ALLOYDB_AI_NL_DATABASE' not set") - case AlloyDBAINLUser: - t.Fatal("'ALLOYDB_AI_NL_USER' not set") - case AlloyDBAINLPass: - t.Fatal("'ALLOYDB_AI_NL_PASS' not set") - } - return map[string]any{ - "type": AlloyDBAINLSourceType, - "project": AlloyDBAINLProject, - "cluster": AlloyDBAINLCluster, - "instance": AlloyDBAINLInstance, - "region": AlloyDBAINLRegion, - "database": AlloyDBAINLDatabase, - "user": AlloyDBAINLUser, - "password": AlloyDBAINLPass, - } -} - func TestAlloyDBAINLToolEndpoints(t *testing.T) { sourceConfig := getAlloyDBAINLVars(t) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) @@ -277,59 +235,6 @@ func runAINLToolInvokeTest(t *testing.T) { } -func getAINLToolsConfig(sourceConfig map[string]any) map[string]any { - // Write config into a file and pass it to command - toolsFile := map[string]any{ - "sources": map[string]any{ - "my-instance": sourceConfig, - }, - "authServices": map[string]any{ - "my-google-auth": map[string]any{ - "type": "google", - "clientId": tests.ClientId, - }, - }, - "tools": map[string]any{ - "my-simple-tool": map[string]any{ - "type": AlloyDBAINLToolType, - "source": "my-instance", - "description": "Simple tool to test end to end functionality.", - "nlConfig": "my_nl_config", - }, - "my-auth-tool": map[string]any{ - "type": AlloyDBAINLToolType, - "source": "my-instance", - "description": "Tool to test authenticated parameters.", - "nlConfig": "my_nl_config", - "nlConfigParameters": []map[string]any{ - { - "name": "email", - "type": "string", - "description": "user email", - "authServices": []map[string]string{ - { - "name": "my-google-auth", - "field": "email", - }, - }, - }, - }, - }, - "my-auth-required-tool": map[string]any{ - "type": AlloyDBAINLToolType, - "source": "my-instance", - "description": "Tool to test auth required invocation.", - "nlConfig": "my_nl_config", - "authRequired": []string{ - "my-google-auth", - }, - }, - }, - } - - return toolsFile -} - func runAINLMCPToolCallMethod(t *testing.T) { sessionId := tests.RunInitialize(t, "2024-11-05") header := map[string]string{} diff --git a/tests/alloydbainl/alloydb_ai_nl_mcp_test.go b/tests/alloydbainl/alloydb_ai_nl_mcp_test.go new file mode 100644 index 000000000000..b25f8eedb8b5 --- /dev/null +++ b/tests/alloydbainl/alloydb_ai_nl_mcp_test.go @@ -0,0 +1,333 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alloydbainl + +import ( + "context" + "net/http" + "os" + "regexp" + "testing" + "time" + + "github.com/googleapis/mcp-toolbox/internal/testutils" + "github.com/googleapis/mcp-toolbox/tests" +) + +func TestAlloyDBAINLListTools(t *testing.T) { + sourceConfig := getAlloyDBAINLVars(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + toolsFile := getAINLToolsConfig(sourceConfig) + + cmd, cleanup, err := tests.StartCmd(ctx, toolsFile) + if err != nil { + t.Fatalf("command initialization returned an error: %s", err) + } + defer cleanup() + + waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second) + defer cancelWait() + out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) + if err != nil { + t.Logf("toolbox command logs: \n%s", out) + t.Fatalf("toolbox didn't start successfully: %s", err) + } + + // Verify list of tools + expectedTools := []tests.MCPToolManifest{ + { + Name: "my-simple-tool", + Description: "Simple tool to test end to end functionality.", + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "question": map[string]any{ + "description": "The natural language question to ask.", + "type": "string", + }, + }, + "required": []any{"question"}, + }, + }, + { + Name: "my-auth-tool", + Description: "Tool to test authenticated parameters.", + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "question": map[string]any{ + "description": "The natural language question to ask.", + "type": "string", + }, + "email": map[string]any{ + "description": "user email", + "type": "string", + }, + }, + "required": []any{"question", "email"}, + }, + }, + { + Name: "my-auth-required-tool", + Description: "Tool to test auth required invocation.", + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "question": map[string]any{ + "description": "The natural language question to ask.", + "type": "string", + }, + }, + "required": []any{"question"}, + }, + }, + } + + tests.RunMCPToolsListMethod(t, expectedTools) +} + +func TestAlloyDBAINLCallTool(t *testing.T) { + sourceConfig := getAlloyDBAINLVars(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + toolsFile := getAINLToolsConfig(sourceConfig) + + cmd, cleanup, err := tests.StartCmd(ctx, toolsFile) + if err != nil { + t.Fatalf("command initialization returned an error: %s", err) + } + defer cleanup() + + waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second) + defer cancelWait() + out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) + if err != nil { + t.Logf("toolbox command logs: \n%s", out) + t.Fatalf("toolbox didn't start successfully: %s", err) + } + + idToken, err := tests.GetGoogleIdToken(t) + if err != nil { + t.Fatalf("error getting Google ID token: %s", err) + } + + invokeTcs := []struct { + name string + toolName string + args map[string]any + requestHeader map[string]string + want string + isErr bool + wantStatusCode int + }{ + { + name: "invoke my-simple-tool", + toolName: "my-simple-tool", + args: map[string]any{"question": "return the number 1"}, + want: "{\"execute_nl_query\":{\"?column?\":1}}", + isErr: false, + }, + { + name: "Invoke my-auth-tool with auth token", + toolName: "my-auth-tool", + args: map[string]any{"question": "can you show me the name of this user?"}, + requestHeader: map[string]string{"my-google-auth_token": idToken}, + want: "{\"execute_nl_query\":{\"name\":\"Alice\"}}", + isErr: false, + }, + { + name: "Invoke my-auth-tool with invalid auth token", + toolName: "my-auth-tool", + args: map[string]any{"question": "return the number 1"}, + requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"}, + isErr: true, + }, + { + name: "Invoke my-auth-tool without auth token", + toolName: "my-auth-tool", + args: map[string]any{"question": "return the number 1"}, + isErr: true, + }, + { + name: "Invoke my-auth-required-tool with auth token", + toolName: "my-auth-required-tool", + args: map[string]any{"question": "return the number 1"}, + requestHeader: map[string]string{"my-google-auth_token": idToken}, + isErr: false, + want: "{\"execute_nl_query\":{\"?column?\":1}}", + }, + { + name: "Invoke my-auth-required-tool with invalid auth token", + toolName: "my-auth-required-tool", + args: map[string]any{"question": "return the number 1"}, + requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"}, + isErr: true, + wantStatusCode: 401, + }, + { + name: "Invoke my-auth-required-tool without auth token", + toolName: "my-auth-required-tool", + args: map[string]any{"question": "return the number 1"}, + isErr: true, + wantStatusCode: 401, + }, + { + name: "Invoke invalid tool", + toolName: "foo", + args: map[string]any{}, + isErr: true, + }, + { + name: "Invoke my-auth-tool without parameters", + toolName: "my-auth-tool", + args: map[string]any{}, + isErr: true, + }, + } + + for _, tc := range invokeTcs { + t.Run(tc.name, func(t *testing.T) { + statusCode, mcpResp, err := tests.InvokeMCPTool(t, tc.toolName, tc.args, tc.requestHeader) + if err != nil { + t.Fatalf("native error executing %s: %s", tc.toolName, err) + } + + expectedStatus := tc.wantStatusCode + if expectedStatus == 0 { + expectedStatus = http.StatusOK + } + if statusCode != expectedStatus { + t.Fatalf("expected status %d, got %d", expectedStatus, statusCode) + } + + if tc.isErr { + if mcpResp.Error == nil && !mcpResp.Result.IsError { + t.Fatalf("expected error result or JSON-RPC error, got success") + } + } else { + if mcpResp.Error != nil { + t.Fatalf("expected success, got JSON-RPC error: %v", mcpResp.Error) + } + if mcpResp.Result.IsError { + t.Fatalf("expected success result, got tool error: %v", mcpResp.Result) + } + if len(mcpResp.Result.Content) == 0 { + t.Fatalf("expected at least one content item, got none") + } + got := mcpResp.Result.Content[0].Text + if got != tc.want { + t.Fatalf("unexpected value: got %q, want %q", got, tc.want) + } + } + }) + } +} + +var ( + AlloyDBAINLSourceType = "alloydb-postgres" + AlloyDBAINLToolType = "alloydb-ai-nl" + AlloyDBAINLProject = os.Getenv("ALLOYDB_AI_NL_PROJECT") + AlloyDBAINLRegion = os.Getenv("ALLOYDB_AI_NL_REGION") + AlloyDBAINLCluster = os.Getenv("ALLOYDB_AI_NL_CLUSTER") + AlloyDBAINLInstance = os.Getenv("ALLOYDB_AI_NL_INSTANCE") + AlloyDBAINLDatabase = os.Getenv("ALLOYDB_AI_NL_DATABASE") + AlloyDBAINLUser = os.Getenv("ALLOYDB_AI_NL_USER") + AlloyDBAINLPass = os.Getenv("ALLOYDB_AI_NL_PASS") +) + +func getAlloyDBAINLVars(t *testing.T) map[string]any { + switch "" { + case AlloyDBAINLProject: + t.Fatal("'ALLOYDB_AI_NL_PROJECT' not set") + case AlloyDBAINLRegion: + t.Fatal("'ALLOYDB_AI_NL_REGION' not set") + case AlloyDBAINLCluster: + t.Fatal("'ALLOYDB_AI_NL_CLUSTER' not set") + case AlloyDBAINLInstance: + t.Fatal("'ALLOYDB_AI_NL_INSTANCE' not set") + case AlloyDBAINLDatabase: + t.Fatal("'ALLOYDB_AI_NL_DATABASE' not set") + case AlloyDBAINLUser: + t.Fatal("'ALLOYDB_AI_NL_USER' not set") + case AlloyDBAINLPass: + t.Fatal("'ALLOYDB_AI_NL_PASS' not set") + } + return map[string]any{ + "type": AlloyDBAINLSourceType, + "project": AlloyDBAINLProject, + "cluster": AlloyDBAINLCluster, + "instance": AlloyDBAINLInstance, + "region": AlloyDBAINLRegion, + "database": AlloyDBAINLDatabase, + "user": AlloyDBAINLUser, + "password": AlloyDBAINLPass, + } +} + +func getAINLToolsConfig(sourceConfig map[string]any) map[string]any { + // Write config into a file and pass it to command + toolsFile := map[string]any{ + "sources": map[string]any{ + "my-instance": sourceConfig, + }, + "authServices": map[string]any{ + "my-google-auth": map[string]any{ + "type": "google", + "clientId": tests.ClientId, + }, + }, + "tools": map[string]any{ + "my-simple-tool": map[string]any{ + "type": AlloyDBAINLToolType, + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + "nlConfig": "my_nl_config", + }, + "my-auth-tool": map[string]any{ + "type": AlloyDBAINLToolType, + "source": "my-instance", + "description": "Tool to test authenticated parameters.", + "nlConfig": "my_nl_config", + "nlConfigParameters": []map[string]any{ + { + "name": "email", + "type": "string", + "description": "user email", + "authServices": []map[string]string{ + { + "name": "my-google-auth", + "field": "email", + }, + }, + }, + }, + }, + "my-auth-required-tool": map[string]any{ + "type": AlloyDBAINLToolType, + "source": "my-instance", + "description": "Tool to test auth required invocation.", + "nlConfig": "my_nl_config", + "authRequired": []string{ + "my-google-auth", + }, + }, + }, + } + + return toolsFile +} diff --git a/tests/auth/auth_integration_test.go b/tests/auth/auth_integration_test.go index e434ac8c2d38..990c769b3c93 100644 --- a/tests/auth/auth_integration_test.go +++ b/tests/auth/auth_integration_test.go @@ -65,6 +65,16 @@ func TestMcpAuth(t *testing.T) { }) return } + if r.URL.Path == "/introspect" { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "active": true, + "scope": "read:files", + "aud": "test-audience", + "exp": time.Now().Add(time.Hour).Unix(), + }) + return + } http.NotFound(w, r) })) defer jwksServer.Close() @@ -82,7 +92,7 @@ func TestMcpAuth(t *testing.T) { }, "tools": map[string]any{}, } - args := []string{"--enable-api"} + args := []string{"--enable-api", "--toolbox-url=http://127.0.0.1:5000"} cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) @@ -99,73 +109,85 @@ func TestMcpAuth(t *testing.T) { api := "http://127.0.0.1:5000/mcp/sse" - t.Run("401 Unauthorized without token", func(t *testing.T) { - req, _ := http.NewRequest(http.MethodGet, api, nil) - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("unable to send request: %s", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusUnauthorized { - t.Fatalf("expected 401, got %d", resp.StatusCode) - } - authHeader := resp.Header.Get("WWW-Authenticate") - if !strings.Contains(authHeader, `resource_metadata="/.well-known/oauth-protected-resource"`) || !strings.Contains(authHeader, `scope="read:files"`) { - t.Fatalf("expected WWW-Authenticate header to contain resource_metadata and scope, got: %s", authHeader) - } + // Generate invalid token (wrong scopes) + invalidToken := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "aud": "test-audience", + "scope": "wrong:scope", + "sub": "test-user", + "exp": time.Now().Add(time.Hour).Unix(), }) - - t.Run("403 Forbidden with insufficient scopes", func(t *testing.T) { - // Generate valid token but wrong scopes - token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "aud": "test-audience", - "scope": "wrong:scope", - "sub": "test-user", - "exp": time.Now().Add(time.Hour).Unix(), - }) - token.Header["kid"] = "test-key-id" - signedString, _ := token.SignedString(privateKey) - - req, _ := http.NewRequest(http.MethodGet, api, nil) - req.Header.Add("Authorization", "Bearer "+signedString) - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("unable to send request: %s", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusForbidden { - t.Fatalf("expected 403, got %d", resp.StatusCode) - } - authHeader := resp.Header.Get("WWW-Authenticate") - if !strings.Contains(authHeader, `resource_metadata="/.well-known/oauth-protected-resource"`) || !strings.Contains(authHeader, `scope="read:files"`) || !strings.Contains(authHeader, `error="insufficient_scope"`) { - t.Fatalf("expected WWW-Authenticate header to contain error, scope, and resource_metadata, got: %s", authHeader) - } + invalidToken.Header["kid"] = "test-key-id" + invalidSignedString, _ := invalidToken.SignedString(privateKey) + + // Generate valid token (correct scopes) + validToken := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "aud": "test-audience", + "scope": "read:files", + "sub": "test-user", + "exp": time.Now().Add(time.Hour).Unix(), }) + validToken.Header["kid"] = "test-key-id" + validSignedString, _ := validToken.SignedString(privateKey) + + tests := []struct { + name string + token string + wantStatusCode int + checkWWWAuth func(t *testing.T, authHeader string) + }{ + { + name: "401 Unauthorized without token", + token: "", + wantStatusCode: http.StatusUnauthorized, + checkWWWAuth: func(t *testing.T, authHeader string) { + if !strings.Contains(authHeader, `resource_metadata="http://127.0.0.1:5000/.well-known/oauth-protected-resource"`) || !strings.Contains(authHeader, `scope="read:files"`) { + t.Fatalf("expected WWW-Authenticate header to contain resource_metadata and scope, got: %s", authHeader) + } + }, + }, + { + name: "403 Forbidden with insufficient scopes", + token: invalidSignedString, + wantStatusCode: http.StatusForbidden, + checkWWWAuth: func(t *testing.T, authHeader string) { + if !strings.Contains(authHeader, `resource_metadata="http://127.0.0.1:5000/.well-known/oauth-protected-resource"`) || !strings.Contains(authHeader, `scope="read:files"`) || !strings.Contains(authHeader, `error="insufficient_scope"`) { + t.Fatalf("expected WWW-Authenticate header to contain error, scope, and resource_metadata, got: %s", authHeader) + } + }, + }, + { + name: "200 OK with valid token", + token: validSignedString, + wantStatusCode: http.StatusOK, + }, + { + name: "200 OK with valid opaque token", + token: "this-is-an-opaque-token", + wantStatusCode: http.StatusOK, + }, + } - t.Run("200 OK with valid token", func(t *testing.T) { - // Generate valid token with correct scopes - token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "aud": "test-audience", - "scope": "read:files", - "sub": "test-user", - "exp": time.Now().Add(time.Hour).Unix(), - }) - token.Header["kid"] = "test-key-id" - signedString, _ := token.SignedString(privateKey) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, api, nil) + if tc.token != "" { + req.Header.Add("Authorization", "Bearer "+tc.token) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("unable to send request: %s", err) + } + defer resp.Body.Close() - req, _ := http.NewRequest(http.MethodGet, api, nil) - req.Header.Add("Authorization", "Bearer "+signedString) - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("unable to send request: %s", err) - } - defer resp.Body.Close() + if resp.StatusCode != tc.wantStatusCode { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("expected %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) + } - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(bodyBytes)) - } - }) + if tc.checkWWWAuth != nil { + authHeader := resp.Header.Get("WWW-Authenticate") + tc.checkWWWAuth(t, authHeader) + } + }) + } } diff --git a/tests/bigquery/bigquery_integration_test.go b/tests/bigquery/bigquery_integration_test.go index 1ca2bb207179..5b4bf8de34f7 100644 --- a/tests/bigquery/bigquery_integration_test.go +++ b/tests/bigquery/bigquery_integration_test.go @@ -156,6 +156,14 @@ func TestBigQueryToolEndpoints(t *testing.T) { tmplSelectCombined, tmplSelectFilterCombined := getBigQueryTmplToolStatement() toolsFile = tests.AddTemplateParamConfig(t, toolsFile, BigqueryToolType, tmplSelectCombined, tmplSelectFilterCombined, "") + // Set up table for semantic search + vectorTableName, teardownVectorTable := setupBigQueryVectorTable(t, ctx, client, datasetName) + defer teardownVectorTable(t) + + // Add semantic search tool config + insertStmt, searchStmt := getBigQueryVectorSearchStmts(vectorTableName) + toolsFile = tests.AddSemanticSearchConfig(t, toolsFile, BigqueryToolType, insertStmt, searchStmt) + cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) @@ -205,6 +213,7 @@ func TestBigQueryToolEndpoints(t *testing.T) { runBigQueryGetTableInfoToolInvokeTest(t, datasetName, tableName, tableInfoWant) runBigQueryConversationalAnalyticsInvokeTest(t, datasetName, tableName, dataInsightsWant) runBigQuerySearchCatalogToolInvokeTest(t, datasetName, tableName) + tests.RunSemanticSearchToolInvokeTest(t, ddlWant, "", "The quick brown fox") } func TestBigQueryToolWithDatasetRestriction(t *testing.T) { @@ -3263,3 +3272,24 @@ func runAnalyzeContributionWithRestriction(t *testing.T, allowedTableFullName, d }) } } + +// setupBigQueryVectorTable creates a vector table in BigQuery for semantic search testing +func setupBigQueryVectorTable(t *testing.T, ctx context.Context, client *bigqueryapi.Client, datasetName string) (string, func(*testing.T)) { + tableName := fmt.Sprintf("vector_table_%s", strings.ReplaceAll(uuid.New().String(), "-", "")) + fullTableName := fmt.Sprintf("`%s.%s.%s`", BigqueryProject, datasetName, tableName) + createStatement := fmt.Sprintf(`CREATE TABLE %s ( + id INT64, + content STRING, + embedding ARRAY + )`, fullTableName) + + teardownTable := setupBigQueryTable(t, ctx, client, createStatement, "", datasetName, fullTableName, nil) + return fullTableName, teardownTable +} + +// getBigQueryVectorSearchStmts returns statements for bigquery semantic search +func getBigQueryVectorSearchStmts(vectorTableName string) (string, string) { + insertStmt := fmt.Sprintf("INSERT INTO %s (id, content, embedding) VALUES (1, @content, @text_to_embed)", vectorTableName) + searchStmt := fmt.Sprintf("SELECT id, content, ML.DISTANCE(embedding, @query, 'COSINE') AS distance FROM %s ORDER BY distance LIMIT 1", vectorTableName) + return insertStmt, searchStmt +} diff --git a/tests/elasticsearch/elasticsearch_integration_test.go b/tests/elasticsearch/elasticsearch_integration_test.go index f3357a50eb16..bded6f319c12 100644 --- a/tests/elasticsearch/elasticsearch_integration_test.go +++ b/tests/elasticsearch/elasticsearch_integration_test.go @@ -15,8 +15,11 @@ package elasticsearch import ( + "bytes" "context" + "encoding/json" "fmt" + "net/http" "os" "regexp" "strings" @@ -146,6 +149,9 @@ func TestElasticsearchToolEndpoints(t *testing.T) { tests.WithNullWant(wants.Null), ) tests.RunMCPToolCallMethod(t, wants.McpMyFailTool, wants.McpSelect1, tests.WithMcpMyToolId3NameAliceWant(wants.McpMyToolId3NameAlice)) + + runExecuteEsqlTest(t, index) + } func getElasticsearchQueries(index string) (string, string, string, string, string) { @@ -303,7 +309,39 @@ func getElasticsearchToolsConfig(sourceConfig map[string]any, toolType, paramToo "description": "Tool to test statement with incorrect syntax.", "query": "SELEC 1;", }, + "my-execute-tool": map[string]any{ + "type": "elasticsearch-execute-esql", + "source": "my-instance", + "description": "Tool to test arbitrary ES|QL execution.", + }, }, } return toolsFile } + +func runExecuteEsqlTest(t *testing.T, index string) { + t.Run("invoke my-execute-tool", func(t *testing.T) { + api := "http://127.0.0.1:5000/api/tool/my-execute-tool/invoke" + reqBody := map[string]any{ + "query": fmt.Sprintf("FROM %s | KEEP id | SORT id ASC", index), + } + bodyBytes, _ := json.Marshal(reqBody) + resp, respBody := tests.RunRequest(t, http.MethodPost, api, bytes.NewBuffer(bodyBytes), nil) + if resp.StatusCode != http.StatusOK { + t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(respBody)) + } + var body map[string]interface{} + err := json.Unmarshal(respBody, &body) + if err != nil { + t.Fatalf("error parsing response body") + } + got, ok := body["result"].(string) + if !ok { + t.Fatalf("unable to find result in response body") + } + want := `[{"id":1},{"id":2},{"id":3},{"id":4}]` + if got != want { + t.Fatalf("unexpected value: got %q, want %q", got, want) + } + }) +} diff --git a/tests/http/http_integration_test.go b/tests/http/http_integration_test.go index 7e1cc66316e5..84aedf7200a5 100644 --- a/tests/http/http_integration_test.go +++ b/tests/http/http_integration_test.go @@ -20,11 +20,9 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" - "fmt" "io" "net/http" "net/http/httptest" - "reflect" "regexp" "strings" "testing" @@ -33,15 +31,9 @@ import ( "github.com/MicahParks/jwkset" "github.com/golang-jwt/jwt/v5" "github.com/googleapis/mcp-toolbox/internal/testutils" - "github.com/googleapis/mcp-toolbox/internal/util/parameters" "github.com/googleapis/mcp-toolbox/tests" ) -var ( - HttpSourceType = "http" - HttpToolType = "http" -) - func getHTTPSourceConfig(t *testing.T) map[string]any { idToken, err := tests.GetGoogleIdToken(t) if err != nil { @@ -55,252 +47,6 @@ func getHTTPSourceConfig(t *testing.T) map[string]any { } } -// handler function for the test server -func multiTool(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - path = strings.TrimPrefix(path, "/") // Remove leading slash - - switch path { - case "tool0": - handleTool0(w, r) - case "tool1": - handleTool1(w, r) - case "tool1id": - handleTool1Id(w, r) - case "tool1name": - handleTool1Name(w, r) - case "tool2": - handleTool2(w, r) - case "tool3": - handleTool3(w, r) - case "toolQueryTest": - handleQueryTest(w, r) - default: - http.NotFound(w, r) // Return 404 for unknown paths - } -} - -// handleQueryTest simply returns the raw query string it received so the test -// can verify it's formatted correctly. -func handleQueryTest(w http.ResponseWriter, r *http.Request) { - // expect GET method - if r.Method != http.MethodGet { - errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusOK) - enc := json.NewEncoder(w) - enc.SetEscapeHTML(false) - - err := enc.Encode(r.URL.RawQuery) - if err != nil { - http.Error(w, "Failed to write response", http.StatusInternalServerError) - return - } -} - -// handler function for the test server -func handleTool0(w http.ResponseWriter, r *http.Request) { - // expect POST method - if r.Method != http.MethodPost { - errorMessage := fmt.Sprintf("expected POST method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - w.WriteHeader(http.StatusOK) - response := "hello world" - err := json.NewEncoder(w).Encode(response) - if err != nil { - http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) - return - } -} - -// handler function for the test server -func handleTool1(w http.ResponseWriter, r *http.Request) { - // expect GET method - if r.Method != http.MethodGet { - errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - // Parse request body - var requestBody map[string]interface{} - bodyBytes, readErr := io.ReadAll(r.Body) - if readErr != nil { - http.Error(w, "Bad Request: Failed to read request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - err := json.Unmarshal(bodyBytes, &requestBody) - if err != nil { - errorMessage := fmt.Sprintf("Bad Request: Error unmarshalling request body: %s, Raw body: %s", err, string(bodyBytes)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - // Extract name - name, ok := requestBody["name"].(string) - if !ok || name == "" { - http.Error(w, "Bad Request: Missing or invalid name", http.StatusBadRequest) - return - } - - if name == "Alice" { - response := `[{"id":1,"name":"Alice"},{"id":3,"name":"Sid"}]` - _, err := w.Write([]byte(response)) - if err != nil { - http.Error(w, "Failed to write response", http.StatusInternalServerError) - } - return - } - - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) -} - -// handler function for the test server -func handleTool1Id(w http.ResponseWriter, r *http.Request) { - // expect GET method - if r.Method != http.MethodGet { - errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - id := r.URL.Query().Get("id") - if id == "4" { - response := `[{"id":4,"name":null}]` - _, err := w.Write([]byte(response)) - if err != nil { - http.Error(w, "Failed to write response", http.StatusInternalServerError) - } - return - } - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) -} - -// handler function for the test server -func handleTool1Name(w http.ResponseWriter, r *http.Request) { - // expect GET method - if r.Method != http.MethodGet { - errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - if !r.URL.Query().Has("name") { - response := "null" - _, err := w.Write([]byte(response)) - if err != nil { - http.Error(w, "Failed to write response", http.StatusInternalServerError) - } - return - } - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) -} - -// handler function for the test server -func handleTool2(w http.ResponseWriter, r *http.Request) { - // expect GET method - if r.Method != http.MethodGet { - errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - email := r.URL.Query().Get("email") - if email != "" { - response := `[{"name":"Alice"}]` - _, err := w.Write([]byte(response)) - if err != nil { - http.Error(w, "Failed to write response", http.StatusInternalServerError) - } - return - } - - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) -} - -// handler function for the test server -func handleTool3(w http.ResponseWriter, r *http.Request) { - // expect GET method - if r.Method != http.MethodGet { - errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - // Check request headers - expectedHeaders := map[string]string{ - "Content-Type": "application/json", - "X-Custom-Header": "example", - "X-Other-Header": "test", - } - for header, expectedValue := range expectedHeaders { - if r.Header.Get(header) != expectedValue { - errorMessage := fmt.Sprintf("Bad Request: Missing or incorrect header: %s", header) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - } - - // Check query parameters - expectedQueryParams := map[string][]string{ - "id": []string{"2", "1", "3"}, - "country": []string{"US"}, - } - query := r.URL.Query() - for param, expectedValueSlice := range expectedQueryParams { - values, ok := query[param] - if ok { - if !reflect.DeepEqual(expectedValueSlice, values) { - errorMessage := fmt.Sprintf("Bad Request: Incorrect query parameter: %s, actual: %s", param, query[param]) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - } else { - errorMessage := fmt.Sprintf("Bad Request: Missing query parameter: %s, actual: %s", param, query[param]) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - } - - // Parse request body - var requestBody map[string]interface{} - bodyBytes, readErr := io.ReadAll(r.Body) - if readErr != nil { - http.Error(w, "Bad Request: Failed to read request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - err := json.Unmarshal(bodyBytes, &requestBody) - if err != nil { - errorMessage := fmt.Sprintf("Bad Request: Error unmarshalling request body: %s, Raw body: %s", err, string(bodyBytes)) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - // Check request body - expectedBody := map[string]interface{}{ - "place": "zoo", - "animals": []any{"rabbit", "ostrich", "whale"}, - } - - if !reflect.DeepEqual(requestBody, expectedBody) { - errorMessage := fmt.Sprintf("Bad Request: Incorrect request body. Expected: %v, Got: %v", expectedBody, requestBody) - http.Error(w, errorMessage, http.StatusBadRequest) - return - } - - response := "hello world" - err = json.NewEncoder(w).Encode(response) - if err != nil { - http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) - return - } -} - func TestHttpToolEndpoints(t *testing.T) { // start a test server server := httptest.NewServer(http.HandlerFunc(multiTool)) @@ -597,152 +343,3 @@ func runAdvancedHTTPInvokeTest(t *testing.T) { }) } } - -// getHTTPToolsConfig returns a mock HTTP tool's config file -func getHTTPToolsConfig(sourceConfig map[string]any, toolType string, jwksURL string) map[string]any { - // Write config into a file and pass it to command - otherSourceConfig := make(map[string]any) - for k, v := range sourceConfig { - otherSourceConfig[k] = v - } - otherSourceConfig["headers"] = map[string]string{"X-Custom-Header": "unexpected", "Content-Type": "application/json"} - otherSourceConfig["queryParams"] = map[string]any{"id": 1, "name": "Sid"} - - clientID := tests.ClientId - if clientID == "" { - clientID = "test-client-id" - } - - toolsFile := map[string]any{ - "sources": map[string]any{ - "my-instance": sourceConfig, - "other-instance": otherSourceConfig, - }, - "authServices": map[string]any{ - "my-google-auth": map[string]any{ - "type": "google", - "clientId": clientID, - }, - "my-generic-auth": map[string]any{ - "type": "generic", - "audience": "test-audience", - "authorizationServer": jwksURL, - "scopesRequired": []string{"read:files"}, - }, - }, - "tools": map[string]any{ - "my-simple-tool": map[string]any{ - "type": toolType, - "path": "/tool0", - "method": "POST", - "source": "my-instance", - "requestBody": "{}", - "description": "Simple tool to test end to end functionality.", - }, - "my-tool": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "GET", - "path": "/tool1", - "description": "some description", - "queryParams": []parameters.Parameter{ - parameters.NewIntParameter("id", "user ID")}, - "bodyParams": []parameters.Parameter{parameters.NewStringParameter("name", "user name")}, - "requestBody": `{ -"age": 36, -"name": "{{.name}}" -} -`, - "headers": map[string]string{"Content-Type": "application/json"}, - }, - "my-tool-by-id": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "GET", - "path": "/tool1id", - "description": "some description", - "queryParams": []parameters.Parameter{ - parameters.NewIntParameter("id", "user ID")}, - "headers": map[string]string{"Content-Type": "application/json"}, - }, - "my-tool-by-name": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "GET", - "path": "/tool1name", - "description": "some description", - "queryParams": []parameters.Parameter{ - parameters.NewStringParameterWithRequired("name", "user name", false)}, - "headers": map[string]string{"Content-Type": "application/json"}, - }, - "my-query-param-tool": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "GET", - "path": "/toolQueryTest", - "description": "Tool to test optional query parameters.", - "queryParams": []parameters.Parameter{ - parameters.NewStringParameterWithRequired("reqId", "required ID", true), - parameters.NewStringParameterWithRequired("page", "optional page number", false), - parameters.NewStringParameterWithRequired("filter", "optional filter string", false), - }, - }, - "my-auth-tool": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "GET", - "path": "/tool2", - "description": "some description", - "requestBody": "{}", - "queryParams": []parameters.Parameter{ - parameters.NewStringParameterWithAuth("email", "some description", - []parameters.ParamAuthService{{Name: "my-google-auth", Field: "email"}}), - }, - }, - "my-auth-required-tool": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "POST", - "path": "/tool0", - "description": "some description", - "requestBody": "{}", - "authRequired": []string{"my-google-auth"}, - }, - "my-auth-required-generic-tool": map[string]any{ - "type": toolType, - "source": "my-instance", - "method": "POST", - "path": "/tool0", - "description": "some description", - "requestBody": "{}", - "authRequired": []string{"my-generic-auth"}, - }, - "my-advanced-tool": map[string]any{ - "type": toolType, - "source": "other-instance", - "method": "get", - "path": "/{{.path}}?id=2", - "description": "some description", - "headers": map[string]string{ - "X-Custom-Header": "example", - }, - "pathParams": []parameters.Parameter{ - ¶meters.StringParameter{ - CommonParameter: parameters.CommonParameter{Name: "path", Type: "string", Desc: "path param"}, - }, - }, - "queryParams": []parameters.Parameter{ - parameters.NewIntParameter("id", "user ID"), parameters.NewStringParameter("country", "country"), - }, - "requestBody": `{ - "place": "zoo", - "animals": {{json .animalArray }} - } - `, - "bodyParams": []parameters.Parameter{parameters.NewArrayParameter("animalArray", "animals in the zoo", parameters.NewStringParameter("animals", "desc"))}, - "headerParams": []parameters.Parameter{parameters.NewStringParameter("X-Other-Header", "custom header")}, - }, - }, - } - return toolsFile -} diff --git a/tests/http/http_mcp_test.go b/tests/http/http_mcp_test.go index dc94de5136af..cef039563ae5 100644 --- a/tests/http/http_mcp_test.go +++ b/tests/http/http_mcp_test.go @@ -19,18 +19,403 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" + "fmt" + "io" "net/http" "net/http/httptest" + "reflect" "regexp" + "strings" "testing" "time" "github.com/MicahParks/jwkset" "github.com/golang-jwt/jwt/v5" "github.com/googleapis/mcp-toolbox/internal/testutils" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" "github.com/googleapis/mcp-toolbox/tests" ) +var ( + HttpSourceType = "http" + HttpToolType = "http" +) + +// handler function for the test server +func multiTool(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + path = strings.TrimPrefix(path, "/") // Remove leading slash + + switch path { + case "tool0": + handleTool0(w, r) + case "tool1": + handleTool1(w, r) + case "tool1id": + handleTool1Id(w, r) + case "tool1name": + handleTool1Name(w, r) + case "tool2": + handleTool2(w, r) + case "tool3": + handleTool3(w, r) + case "toolQueryTest": + handleQueryTest(w, r) + default: + http.NotFound(w, r) // Return 404 for unknown paths + } +} + +// handleQueryTest simply returns the raw query string it received so the test +// can verify it's formatted correctly. +func handleQueryTest(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + + err := enc.Encode(r.URL.RawQuery) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } +} + +func handleTool0(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + errorMessage := fmt.Sprintf("expected POST method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + response := "hello world" + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) + return + } +} + +func handleTool1(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + var requestBody map[string]interface{} + bodyBytes, readErr := io.ReadAll(r.Body) + if readErr != nil { + http.Error(w, "Bad Request: Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + err := json.Unmarshal(bodyBytes, &requestBody) + if err != nil { + errorMessage := fmt.Sprintf("Bad Request: Error unmarshalling request body: %s, Raw body: %s", err, string(bodyBytes)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + name, ok := requestBody["name"].(string) + if !ok || name == "" { + http.Error(w, "Bad Request: Missing or invalid name", http.StatusBadRequest) + return + } + + if name == "Alice" { + response := `[{"id":1,"name":"Alice"},{"id":3,"name":"Sid"}]` + _, err := w.Write([]byte(response)) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + } + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +func handleTool1Id(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + id := r.URL.Query().Get("id") + if id == "4" { + response := `[{"id":4,"name":null}]` + _, err := w.Write([]byte(response)) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + } + return + } + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +func handleTool1Name(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + if !r.URL.Query().Has("name") { + response := "null" + _, err := w.Write([]byte(response)) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + } + return + } + + http.Error(w, "Bad Request: Unexpected query parameter 'name'", http.StatusBadRequest) +} + +func handleTool2(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + email := r.URL.Query().Get("email") + if email != "" { + response := `[{"name":"Alice"}]` + _, err := w.Write([]byte(response)) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + } + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +func handleTool3(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + expectedHeaders := map[string]string{ + "Content-Type": "application/json", + "X-Custom-Header": "example", + "X-Other-Header": "test", + } + for header, expectedValue := range expectedHeaders { + if r.Header.Get(header) != expectedValue { + errorMessage := fmt.Sprintf("Bad Request: Missing or incorrect header: %s", header) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + } + + expectedQueryParams := map[string][]string{ + "id": []string{"2", "1", "3"}, + "country": []string{"US"}, + } + query := r.URL.Query() + for param, expectedValueSlice := range expectedQueryParams { + values, ok := query[param] + if ok { + if !reflect.DeepEqual(expectedValueSlice, values) { + errorMessage := fmt.Sprintf("Bad Request: Incorrect query parameter: %s, actual: %s", param, query[param]) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + } else { + errorMessage := fmt.Sprintf("Bad Request: Missing query parameter: %s, actual: %s", param, query[param]) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + } + + var requestBody map[string]interface{} + bodyBytes, readErr := io.ReadAll(r.Body) + if readErr != nil { + http.Error(w, "Bad Request: Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + err := json.Unmarshal(bodyBytes, &requestBody) + if err != nil { + errorMessage := fmt.Sprintf("Bad Request: Error unmarshalling request body: %s, Raw body: %s", err, string(bodyBytes)) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + expectedBody := map[string]interface{}{ + "place": "zoo", + "animals": []any{"rabbit", "ostrich", "whale"}, + } + + if !reflect.DeepEqual(requestBody, expectedBody) { + errorMessage := fmt.Sprintf("Bad Request: Incorrect request body. Expected: %v, Got: %v", expectedBody, requestBody) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + response := "hello world" + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) + return + } +} + +func getHTTPToolsConfig(sourceConfig map[string]any, toolType string, jwksURL string) map[string]any { + otherSourceConfig := make(map[string]any) + for k, v := range sourceConfig { + otherSourceConfig[k] = v + } + otherSourceConfig["headers"] = map[string]string{"X-Custom-Header": "unexpected", "Content-Type": "application/json"} + otherSourceConfig["queryParams"] = map[string]any{"id": 1, "name": "Sid"} + + clientID := tests.ClientId + if clientID == "" { + clientID = "test-client-id" + } + + toolsFile := map[string]any{ + "sources": map[string]any{ + "my-instance": sourceConfig, + "other-instance": otherSourceConfig, + }, + "authServices": map[string]any{ + "my-google-auth": map[string]any{ + "type": "google", + "clientId": clientID, + }, + "my-generic-auth": map[string]any{ + "type": "generic", + "audience": "test-audience", + "authorizationServer": jwksURL, + "scopesRequired": []string{"read:files"}, + }, + }, + "tools": map[string]any{ + "my-simple-tool": map[string]any{ + "type": toolType, + "path": "/tool0", + "method": "POST", + "source": "my-instance", + "requestBody": "{}", + "description": "Simple tool to test end to end functionality.", + }, + "my-tool": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "GET", + "path": "/tool1", + "description": "some description", + "queryParams": []parameters.Parameter{ + parameters.NewIntParameter("id", "user ID")}, + "bodyParams": []parameters.Parameter{parameters.NewStringParameter("name", "user name")}, + "requestBody": `{ +"age": 36, +"name": "{{.name}}" +} +`, + "headers": map[string]string{"Content-Type": "application/json"}, + }, + "my-tool-by-id": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "GET", + "path": "/tool1id", + "description": "some description", + "queryParams": []parameters.Parameter{ + parameters.NewIntParameter("id", "user ID")}, + "headers": map[string]string{"Content-Type": "application/json"}, + }, + "my-tool-by-name": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "GET", + "path": "/tool1name", + "description": "some description", + "queryParams": []parameters.Parameter{ + parameters.NewStringParameterWithRequired("name", "user name", false)}, + "headers": map[string]string{"Content-Type": "application/json"}, + }, + "my-query-param-tool": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "GET", + "path": "/toolQueryTest", + "description": "Tool to test optional query parameters.", + "queryParams": []parameters.Parameter{ + parameters.NewStringParameterWithRequired("reqId", "required ID", true), + parameters.NewStringParameterWithRequired("page", "optional page number", false), + parameters.NewStringParameterWithRequired("filter", "optional filter string", false), + }, + }, + "my-auth-tool": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "GET", + "path": "/tool2", + "description": "some description", + "requestBody": "{}", + "queryParams": []parameters.Parameter{ + parameters.NewStringParameterWithAuth("email", "some description", + []parameters.ParamAuthService{{Name: "my-google-auth", Field: "email"}}), + }, + }, + "my-auth-required-tool": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "POST", + "path": "/tool0", + "description": "some description", + "requestBody": "{}", + "authRequired": []string{"my-google-auth"}, + }, + "my-auth-required-generic-tool": map[string]any{ + "type": toolType, + "source": "my-instance", + "method": "POST", + "path": "/tool0", + "description": "some description", + "requestBody": "{}", + "authRequired": []string{"my-generic-auth"}, + }, + "my-advanced-tool": map[string]any{ + "type": toolType, + "source": "other-instance", + "method": "get", + "path": "/{{.path}}?id=2", + "description": "some description", + "headers": map[string]string{ + "X-Custom-Header": "example", + }, + "pathParams": []parameters.Parameter{ + ¶meters.StringParameter{ + CommonParameter: parameters.CommonParameter{Name: "path", Type: "string", Desc: "path param"}, + }, + }, + "queryParams": []parameters.Parameter{ + parameters.NewIntParameter("id", "user ID"), parameters.NewStringParameter("country", "country"), + }, + "requestBody": `{ + "place": "zoo", + "animals": {{json .animalArray }} + } + `, + "bodyParams": []parameters.Parameter{parameters.NewArrayParameter("animalArray", "animals in the zoo", parameters.NewStringParameter("animals", "desc"))}, + "headerParams": []parameters.Parameter{parameters.NewStringParameter("X-Other-Header", "custom header")}, + }, + }, + } + return toolsFile +} + func getMCPHTTPSourceConfig(t *testing.T) map[string]any { idToken, err := tests.GetGoogleIdToken(t) if err != nil { @@ -86,7 +471,37 @@ func TestHTTPListTools(t *testing.T) { })) defer jwksServer.Close() - toolsFile := getHTTPToolsConfig(sourceConfig, HttpToolType, jwksServer.URL) + toolsFile := map[string]any{ + "sources": map[string]any{ + "my-instance": sourceConfig, + }, + "tools": map[string]any{ + "my-simple-tool": map[string]any{ + "type": HttpToolType, + "path": "/tool0", + "method": "POST", + "source": "my-instance", + "requestBody": "{}", + "description": "Simple tool to test end to end functionality.", + }, + "my-tool": map[string]any{ + "type": HttpToolType, + "source": "my-instance", + "method": "GET", + "path": "/tool1", + "description": "some description", + "queryParams": []parameters.Parameter{ + parameters.NewIntParameter("id", "user ID")}, + "bodyParams": []parameters.Parameter{parameters.NewStringParameter("name", "user name")}, + "requestBody": `{ +"age": 36, +"name": "{{.name}}" +} +`, + "headers": map[string]string{"Content-Type": "application/json"}, + }, + }, + } // Start the toolbox server. cmd, cleanup, err := tests.StartCmd(ctx, toolsFile) diff --git a/tests/looker/looker_integration_test.go b/tests/looker/looker_integration_test.go index 7a8c239706f9..e5656bb3329d 100644 --- a/tests/looker/looker_integration_test.go +++ b/tests/looker/looker_integration_test.go @@ -288,8 +288,28 @@ func TestLooker(t *testing.T) { "source": "my-instance", "description": "Simple tool to test end to end functionality.", }, - "project_git_branch": map[string]any{ - "type": "looker-git-branch", + "list_git_branches": map[string]any{ + "type": "looker-list-git-branches", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, + "get_git_branch": map[string]any{ + "type": "looker-get-git-branch", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, + "create_git_branch": map[string]any{ + "type": "looker-create-git-branch", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, + "switch_git_branch": map[string]any{ + "type": "looker-switch-git-branch", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, + "delete_git_branch": map[string]any{ + "type": "looker-delete-git-branch", "source": "my-instance", "description": "Simple tool to test end to end functionality.", }, @@ -1848,9 +1868,9 @@ func TestLooker(t *testing.T) { }, }, ) - tests.RunToolGetTestByName(t, "project_git_branch", + tests.RunToolGetTestByName(t, "list_git_branches", map[string]any{ - "project_git_branch": map[string]any{ + "list_git_branches": map[string]any{ "description": "Simple tool to test end to end functionality.", "authRequired": []any{}, "parameters": []any{ @@ -1861,29 +1881,6 @@ func TestLooker(t *testing.T) { "required": true, "type": "string", }, - map[string]any{ - "authServices": []any{}, - "description": "The operation, one of `list`, `get`, `create`, `switch`, or `delete`", - "name": "operation", - "required": true, - "type": "string", - }, - map[string]any{ - "authServices": []any{}, - "description": "The git branch on which to operate. Not required for `list` or `get` operations.", - "name": "branch", - "required": false, - "type": "string", - "default": "", - }, - map[string]any{ - "authServices": []any{}, - "description": "The ref to use as the start of a new branch. If not specified for a `create` operation it will default to HEAD of current branch. If supplied with a `switch` operation will `reset --hard` the branch.", - "name": "ref", - "required": false, - "type": "string", - "default": "", - }, }, }, }, @@ -1963,23 +1960,23 @@ func TestLooker(t *testing.T) { tests.RunToolInvokeParametersTest(t, "validate_project", []byte(`{"project_id": "the_look"}`), wantResult) wantResult = "master" - tests.RunToolInvokeParametersTest(t, "project_git_branch", []byte(`{"operation": "list", "project_id": "the_look"}`), wantResult) + tests.RunToolInvokeParametersTest(t, "list_git_branches", []byte(`{"project_id": "the_look"}`), wantResult) wantResult = fmt.Sprintf("test_branch_%s", randstr) - tests.RunToolInvokeParametersTest(t, "project_git_branch", []byte(fmt.Sprintf(`{"operation": "create", "project_id": "the_look", "branch": "test_branch_%s"}`, randstr)), wantResult) + tests.RunToolInvokeParametersTest(t, "create_git_branch", []byte(fmt.Sprintf(`{"project_id": "the_look", "branch": "test_branch_%s"}`, randstr)), wantResult) time.Sleep(5 * time.Second) wantResult = "d2d4eafdf8932837b2a12b773282c165a43fb0c0" - tests.RunToolInvokeParametersTest(t, "project_git_branch", []byte(fmt.Sprintf(`{"operation": "switch", "project_id": "the_look", "branch": "test_branch_%s", "ref": "d2d4eafdf8932837b2a12b773282c165a43fb0c0"}`, randstr)), wantResult) + tests.RunToolInvokeParametersTest(t, "switch_git_branch", []byte(fmt.Sprintf(`{"project_id": "the_look", "branch": "test_branch_%s", "ref": "d2d4eafdf8932837b2a12b773282c165a43fb0c0"}`, randstr)), wantResult) wantResult = fmt.Sprintf("test_branch_%s", randstr) - tests.RunToolInvokeParametersTest(t, "project_git_branch", []byte(`{"operation": "get", "project_id": "the_look"}`), wantResult) + tests.RunToolInvokeParametersTest(t, "get_git_branch", []byte(`{"project_id": "the_look"}`), wantResult) wantResult = "dev-mike-deangelo-twqb" - tests.RunToolInvokeParametersTest(t, "project_git_branch", []byte(`{"operation": "switch", "project_id": "the_look", "branch": "dev-mike-deangelo-twqb"}`), wantResult) + tests.RunToolInvokeParametersTest(t, "switch_git_branch", []byte(`{"project_id": "the_look", "branch": "dev-mike-deangelo-twqb"}`), wantResult) wantResult = "Deleted" - tests.RunToolInvokeParametersTest(t, "project_git_branch", []byte(fmt.Sprintf(`{"operation": "delete", "project_id": "the_look", "branch": "test_branch_%s"}`, randstr)), wantResult) + tests.RunToolInvokeParametersTest(t, "delete_git_branch", []byte(fmt.Sprintf(`{"project_id": "the_look", "branch": "test_branch_%s"}`, randstr)), wantResult) wantResult = "[]" tests.RunToolInvokeParametersTest(t, "get_lookml_tests", []byte(`{"project_id": "the_look"}`), wantResult) diff --git a/tests/mcp_tool.go b/tests/mcp_tool.go index d76ab10f26f6..6454db8d62ae 100644 --- a/tests/mcp_tool.go +++ b/tests/mcp_tool.go @@ -18,6 +18,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "net/http" "reflect" "strings" @@ -29,6 +30,78 @@ import ( v20251125 "github.com/googleapis/mcp-toolbox/internal/server/mcp/v20251125" ) +// RunRequest is a helper function to send HTTP requests and return the response +func RunRequest(t *testing.T, method, url string, body io.Reader, headers map[string]string) (*http.Response, []byte) { + req, err := http.NewRequest(method, url, body) + if err != nil { + t.Fatalf("unable to create request: %s", err) + } + + req.Header.Set("Content-type", "application/json") + + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("unable to send request: %s", err) + } + respBody, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unable to read request body: %s", err) + } + + defer resp.Body.Close() + return resp, respBody +} + +// RunInitialize runs the initialize lifecycle for mcp to set up client-server connection +func RunInitialize(t *testing.T, protocolVersion string) string { + url := "http://127.0.0.1:5000/mcp" + + initializeRequestBody := map[string]any{ + "jsonrpc": "2.0", + "id": "mcp-initialize", + "method": "initialize", + "params": map[string]any{ + "protocolVersion": protocolVersion, + }, + } + reqMarshal, err := json.Marshal(initializeRequestBody) + if err != nil { + t.Fatalf("unexpected error during marshaling of body") + } + + resp, _ := RunRequest(t, http.MethodPost, url, bytes.NewBuffer(reqMarshal), nil) + if resp.StatusCode != 200 { + t.Fatalf("response status code is not 200") + } + + if contentType := resp.Header.Get("Content-type"); contentType != "application/json" { + t.Fatalf("unexpected content-type header: want %s, got %s", "application/json", contentType) + } + + sessionId := resp.Header.Get("Mcp-Session-Id") + + header := map[string]string{} + if sessionId != "" { + header["Mcp-Session-Id"] = sessionId + } + + initializeNotificationBody := map[string]any{ + "jsonrpc": "2.0", + "method": "notifications/initialized", + } + notiMarshal, err := json.Marshal(initializeNotificationBody) + if err != nil { + t.Fatalf("unexpected error during marshaling of notifications body") + } + + _, _ = RunRequest(t, http.MethodPost, url, bytes.NewBuffer(notiMarshal), header) + return sessionId +} + // NewMCPRequestHeader takes custom headers and appends headers required for MCP. func NewMCPRequestHeader(t *testing.T, customHeaders map[string]string) map[string]string { headers := make(map[string]string) @@ -143,6 +216,10 @@ func RunMCPToolsListMethod(t *testing.T, expectedOutput []MCPToolManifest) { t.Fatalf("error unmarshalling tools into MCPToolManifest: %v", err) } + if len(actualTools) != len(expectedOutput) { + t.Fatalf("expected %d tools, got %d. Actual tools: %+v", len(expectedOutput), len(actualTools), actualTools) + } + for _, expected := range expectedOutput { found := false for _, actual := range actualTools { diff --git a/tests/tool.go b/tests/tool.go index 0b0c207d6f90..3fc087c3e83b 100644 --- a/tests/tool.go +++ b/tests/tool.go @@ -748,52 +748,6 @@ func RunExecuteSqlToolInvokeTest(t *testing.T, createTableStatement, select1Want } } -// RunInitialize runs the initialize lifecycle for mcp to set up client-server connection -func RunInitialize(t *testing.T, protocolVersion string) string { - url := "http://127.0.0.1:5000/mcp" - - initializeRequestBody := map[string]any{ - "jsonrpc": "2.0", - "id": "mcp-initialize", - "method": "initialize", - "params": map[string]any{ - "protocolVersion": protocolVersion, - }, - } - reqMarshal, err := json.Marshal(initializeRequestBody) - if err != nil { - t.Fatalf("unexpected error during marshaling of body") - } - - resp, _ := RunRequest(t, http.MethodPost, url, bytes.NewBuffer(reqMarshal), nil) - if resp.StatusCode != 200 { - t.Fatalf("response status code is not 200") - } - - if contentType := resp.Header.Get("Content-type"); contentType != "application/json" { - t.Fatalf("unexpected content-type header: want %s, got %s", "application/json", contentType) - } - - sessionId := resp.Header.Get("Mcp-Session-Id") - - header := map[string]string{} - if sessionId != "" { - header["Mcp-Session-Id"] = sessionId - } - - initializeNotificationBody := map[string]any{ - "jsonrpc": "2.0", - "method": "notifications/initialized", - } - notiMarshal, err := json.Marshal(initializeNotificationBody) - if err != nil { - t.Fatalf("unexpected error during marshaling of notifications body") - } - - _, _ = RunRequest(t, http.MethodPost, url, bytes.NewBuffer(notiMarshal), header) - return sessionId -} - // RunMCPToolCallMethod runs the tool/call for mcp endpoint func RunMCPToolCallMethod(t *testing.T, myFailToolWant, select1Want string, options ...McpTestOption) { // Resolve options @@ -4767,33 +4721,6 @@ func RunPostgresListStoredProcedureTest(t *testing.T, ctx context.Context, pool } } -// RunRequest is a helper function to send HTTP requests and return the response -func RunRequest(t *testing.T, method, url string, body io.Reader, headers map[string]string) (*http.Response, []byte) { - // Send request - req, err := http.NewRequest(method, url, body) - if err != nil { - t.Fatalf("unable to create request: %s", err) - } - - req.Header.Set("Content-type", "application/json") - - for k, v := range headers { - req.Header.Set(k, v) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("unable to send request: %s", err) - } - respBody, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("unable to read request body: %s", err) - } - - defer resp.Body.Close() - return resp, respBody -} - func RunStatementToolsTest(t *testing.T, tools map[string]string) { for toolName, paramBody := range tools { t.Run(toolName, func(t *testing.T) {