Compare commits

..

1 Commits

Author SHA1 Message Date
Yuan Teoh
f9d7059460 refactor(sources/looker): move source implementation in Invoke() function into Source 2026-01-07 23:39:14 -08:00
86 changed files with 217 additions and 1377 deletions

View File

@@ -340,26 +340,6 @@ steps:
spanner \
spanner || echo "Integration tests failed." # ignore test failures
- id: "spanner-admin"
name: golang:1
waitFor: ["compile-test-binary"]
entrypoint: /bin/bash
env:
- "GOPATH=/gopath"
- "SPANNER_PROJECT=$PROJECT_ID"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv: ["CLIENT_ID"]
volumes:
- name: "go"
path: "/gopath"
args:
- -c
- |
.ci/test_with_coverage.sh \
"Spanner Admin" \
spanneradmin \
spanneradmin || echo "Integration tests failed."
- id: "neo4j"
name: golang:1
waitFor: ["compile-test-binary"]

View File

@@ -51,10 +51,6 @@ ignoreFiles = ["quickstart/shared", "quickstart/python", "quickstart/js", "quick
# Add a new version block here before every release
# The order of versions in this file is mirrored into the dropdown
[[params.versions]]
version = "v0.25.0"
url = "https://googleapis.github.io/genai-toolbox/v0.25.0/"
[[params.versions]]
version = "v0.24.0"
url = "https://googleapis.github.io/genai-toolbox/v0.24.0/"

View File

@@ -1,30 +1,5 @@
# Changelog
## [0.25.0](https://github.com/googleapis/genai-toolbox/compare/v0.24.0...v0.25.0) (2026-01-08)
### Features
* Add `embeddingModel` support ([#2121](https://github.com/googleapis/genai-toolbox/issues/2121)) ([9c62f31](https://github.com/googleapis/genai-toolbox/commit/9c62f313ff5edf0a3b5b8a3e996eba078fba4095))
* Add `allowed-hosts` flag ([#2254](https://github.com/googleapis/genai-toolbox/issues/2254)) ([17b41f6](https://github.com/googleapis/genai-toolbox/commit/17b41f64531b8fe417c28ada45d1992ba430dc1b))
* Add parameter default value to manifest ([#2264](https://github.com/googleapis/genai-toolbox/issues/2264)) ([9d1feca](https://github.com/googleapis/genai-toolbox/commit/9d1feca10810fa42cb4c94a409252f1bd373ee36))
* **snowflake:** Add Snowflake Source and Tools ([#858](https://github.com/googleapis/genai-toolbox/issues/858)) ([b706b5b](https://github.com/googleapis/genai-toolbox/commit/b706b5bc685aeda277f277868bae77d38d5fd7b6))
* **prebuilt/cloud-sql-mysql:** Update CSQL MySQL prebuilt tools to use IAM ([#2202](https://github.com/googleapis/genai-toolbox/issues/2202)) ([731a32e](https://github.com/googleapis/genai-toolbox/commit/731a32e5360b4d6862d81fcb27d7127c655679a8))
* **sources/bigquery:** Make credentials scope configurable ([#2210](https://github.com/googleapis/genai-toolbox/issues/2210)) ([a450600](https://github.com/googleapis/genai-toolbox/commit/a4506009b93771b77fb05ae97044f914967e67ed))
* **sources/trino:** Add ssl verification options and fix docs example ([#2155](https://github.com/googleapis/genai-toolbox/issues/2155)) ([4a4cf1e](https://github.com/googleapis/genai-toolbox/commit/4a4cf1e712b671853678dba99c4dc49dd4fc16a2))
* **tools/looker:** Add ability to set destination folder with `make_look` and `make_dashboard`. ([#2245](https://github.com/googleapis/genai-toolbox/issues/2245)) ([eb79339](https://github.com/googleapis/genai-toolbox/commit/eb793398cd1cc4006d9808ccda5dc7aea5e92bd5))
* **tools/postgressql:** Add tool to list store procedure ([#2156](https://github.com/googleapis/genai-toolbox/issues/2156)) ([cf0fc51](https://github.com/googleapis/genai-toolbox/commit/cf0fc515b57d9b84770076f3c0c5597c4597ef62))
* **tools/postgressql:** Add Parameter `embeddedBy` config support ([#2151](https://github.com/googleapis/genai-toolbox/issues/2151)) ([17b70cc](https://github.com/googleapis/genai-toolbox/commit/17b70ccaa754d15bcc33a1a3ecb7e652520fa600))
### Bug Fixes
* **server:** Add `embeddingModel` config initialization ([#2281](https://github.com/googleapis/genai-toolbox/issues/2281)) ([a779975](https://github.com/googleapis/genai-toolbox/commit/a7799757c9345f99b6d2717841fbf792d364e1a2))
* **sources/cloudgda:** Add import for cloudgda source ([#2217](https://github.com/googleapis/genai-toolbox/issues/2217)) ([7daa411](https://github.com/googleapis/genai-toolbox/commit/7daa4111f4ebfb0a35319fd67a8f7b9f0f99efcf))
* **tools/alloydb-wait-for-operation:** Fix connection message generation ([#2228](https://github.com/googleapis/genai-toolbox/issues/2228)) ([7053fbb](https://github.com/googleapis/genai-toolbox/commit/7053fbb1953653143d39a8510916ea97a91022a6))
* **tools/alloydbainl:** Only add psv when NL Config Param is defined ([#2265](https://github.com/googleapis/genai-toolbox/issues/2265)) ([ef8f3b0](https://github.com/googleapis/genai-toolbox/commit/ef8f3b02f2f38ce94a6ba9acf35d08b9469bef4e))
* **tools/looker:** Looker client OAuth nil pointer error ([#2231](https://github.com/googleapis/genai-toolbox/issues/2231)) ([268700b](https://github.com/googleapis/genai-toolbox/commit/268700bdbf8281de0318d60ca613ed3672990b20))
## [0.24.0](https://github.com/googleapis/genai-toolbox/compare/v0.23.0...v0.24.0) (2025-12-19)

View File

@@ -140,7 +140,7 @@ To install Toolbox as a binary:
>
> ```sh
> # see releases page for other versions
> export VERSION=0.25.0
> export VERSION=0.24.0
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
> chmod +x toolbox
> ```
@@ -153,7 +153,7 @@ To install Toolbox as a binary:
>
> ```sh
> # see releases page for other versions
> export VERSION=0.25.0
> export VERSION=0.24.0
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
> chmod +x toolbox
> ```
@@ -166,7 +166,7 @@ To install Toolbox as a binary:
>
> ```sh
> # see releases page for other versions
> export VERSION=0.25.0
> export VERSION=0.24.0
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
> chmod +x toolbox
> ```
@@ -179,7 +179,7 @@ To install Toolbox as a binary:
>
> ```cmd
> :: see releases page for other versions
> set VERSION=0.25.0
> set VERSION=0.24.0
> curl -o toolbox.exe "https://storage.googleapis.com/genai-toolbox/v%VERSION%/windows/amd64/toolbox.exe"
> ```
>
@@ -191,7 +191,7 @@ To install Toolbox as a binary:
>
> ```powershell
> # see releases page for other versions
> $VERSION = "0.25.0"
> $VERSION = "0.24.0"
> curl.exe -o toolbox.exe "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe"
> ```
>
@@ -204,7 +204,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.25.0
export VERSION=0.24.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -228,7 +228,7 @@ To install from source, ensure you have the latest version of
[Go installed](https://go.dev/doc/install), and then run the following command:
```sh
go install github.com/googleapis/genai-toolbox@v0.25.0
go install github.com/googleapis/genai-toolbox@v0.24.0
```
<!-- {x-release-please-end} -->

View File

@@ -221,7 +221,6 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannerlistgraphs"
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannerlisttables"
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannersql"
_ "github.com/googleapis/genai-toolbox/internal/tools/spanneradmin/spannercreateinstance"
_ "github.com/googleapis/genai-toolbox/internal/tools/sqlite/sqliteexecutesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/sqlite/sqlitesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/tidb/tidbexecutesql"
@@ -268,7 +267,6 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/sources/singlestore"
_ "github.com/googleapis/genai-toolbox/internal/sources/snowflake"
_ "github.com/googleapis/genai-toolbox/internal/sources/spanner"
_ "github.com/googleapis/genai-toolbox/internal/sources/spanneradmin"
_ "github.com/googleapis/genai-toolbox/internal/sources/sqlite"
_ "github.com/googleapis/genai-toolbox/internal/sources/tidb"
_ "github.com/googleapis/genai-toolbox/internal/sources/trino"
@@ -383,9 +381,7 @@ func NewCommand(opts ...Option) *Command {
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
// TODO: Insecure by default. Might consider updating this for v1.0.0
flags.StringSliceVar(&cmd.cfg.AllowedOrigins, "allowed-origins", []string{"*"}, "Specifies a list of origins permitted to access this server. Defaults to '*'.")
flags.StringSliceVar(&cmd.cfg.AllowedHosts, "allowed-hosts", []string{"*"}, "Specifies a list of hosts permitted to access this server. Defaults to '*'.")
// wrap RunE command so that we have access to original Command object
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
@@ -951,7 +947,6 @@ func run(cmd *Command) error {
cmd.cfg.SourceConfigs = finalToolsFile.Sources
cmd.cfg.AuthServiceConfigs = finalToolsFile.AuthServices
cmd.cfg.EmbeddingModelConfigs = finalToolsFile.EmbeddingModels
cmd.cfg.ToolConfigs = finalToolsFile.Tools
cmd.cfg.ToolsetConfigs = finalToolsFile.Toolsets
cmd.cfg.PromptConfigs = finalToolsFile.Prompts

View File

@@ -67,9 +67,6 @@ func withDefaults(c server.ServerConfig) server.ServerConfig {
if c.AllowedOrigins == nil {
c.AllowedOrigins = []string{"*"}
}
if c.AllowedHosts == nil {
c.AllowedHosts = []string{"*"}
}
return c
}
@@ -223,13 +220,6 @@ func TestServerConfigFlags(t *testing.T) {
AllowedOrigins: []string{"http://foo.com", "http://bar.com"},
}),
},
{
desc: "allowed hosts",
args: []string{"--allowed-hosts", "http://foo.com,http://bar.com"},
want: withDefaults(server.ServerConfig{
AllowedHosts: []string{"http://foo.com", "http://bar.com"},
}),
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {

View File

@@ -1 +1 @@
0.25.0
0.24.0

View File

@@ -1,59 +0,0 @@
# Cloud Spanner Admin MCP Server
The Cloud Spanner Admin Model Context Protocol (MCP) Server gives AI-powered development tools the ability to manage your Google Cloud Spanner infrastructure. It supports creating instances.
## Features
An editor configured to use the Cloud Spanner Admin MCP server can use its AI capabilities to help you:
- **Provision & Manage Infrastructure** - Create Cloud Spanner instances
## Prerequisites
* [Node.js](https://nodejs.org/) installed.
* A Google Cloud project with the **Cloud Spanner Admin API** enabled.
* Ensure [Application Default Credentials](https://cloud.google.com/docs/authentication/gcloud) are available in your environment.
* IAM Permissions:
* Cloud Spanner Admin (`roles/spanner.admin`)
## Install & Configuration
In the Antigravity MCP Store, click the "Install" button.
You'll now be able to see all enabled tools in the "Tools" tab.
> [!NOTE]
> If you encounter issues with Windows Defender blocking the execution, you may need to configure an allowlist. See [Configure exclusions for Microsoft Defender Antivirus](https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/configure-exclusions-microsoft-defender-antivirus?view=o365-worldwide) for more details.
## Usage
Once configured, the MCP server will automatically provide Cloud Spanner Admin capabilities to your AI assistant. You can:
* "Create a new Spanner instance named 'my-spanner-instance' in the 'my-gcp-project' project with config 'regional-us-central1', edition 'ENTERPRISE', and 1 node."
## Server Capabilities
The Cloud Spanner Admin MCP server provides the following tools:
| Tool Name | Description |
|:------------------|:---------------------------------|
| `create_instance` | Create a Cloud Spanner instance. |
## Custom MCP Server Configuration
Add the following configuration to your MCP client (e.g., `settings.json` for Gemini CLI, `mcp_config.json` for Antigravity):
```json
{
"mcpServers": {
"spanner-admin": {
"command": "npx",
"args": ["-y", "@toolbox-sdk/server", "--prebuilt", "spanner-admin", "--stdio"]
}
}
}
```
## Documentation
For more information, visit the [Cloud Spanner Admin API documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1).

View File

@@ -1,18 +0,0 @@
---
title: "Featured Articles"
weight: 3
description: Toolbox Medium Blogs
manualLink: "https://medium.com/@mcp_toolbox"
manualLinkTarget: _blank
---
<html>
<head>
<title>Redirecting to Featured Articles</title>
<link rel="canonical" href="https://medium.com/@mcp_toolbox"/>
<meta http-equiv="refresh" content="0;url=https://medium.com/@mcp_toolbox"/>
</head>
<body>
<p>If you are not automatically redirected, please <a href="https://medium.com/@mcp_toolbox">follow this link to our articles</a>.</p>
</body>
</html>

View File

@@ -234,7 +234,7 @@
},
"outputs": [],
"source": [
"version = \"0.25.0\" # x-release-please-version\n",
"version = \"0.24.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -103,7 +103,7 @@ To install Toolbox as a binary on Linux (AMD64):
```sh
# see releases page for other versions
export VERSION=0.25.0
export VERSION=0.24.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
```
@@ -114,7 +114,7 @@ To install Toolbox as a binary on macOS (Apple Silicon):
```sh
# see releases page for other versions
export VERSION=0.25.0
export VERSION=0.24.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
chmod +x toolbox
```
@@ -125,7 +125,7 @@ To install Toolbox as a binary on macOS (Intel):
```sh
# see releases page for other versions
export VERSION=0.25.0
export VERSION=0.24.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
chmod +x toolbox
```
@@ -136,7 +136,7 @@ To install Toolbox as a binary on Windows (Command Prompt):
```cmd
:: see releases page for other versions
set VERSION=0.25.0
set VERSION=0.24.0
curl -o toolbox.exe "https://storage.googleapis.com/genai-toolbox/v%VERSION%/windows/amd64/toolbox.exe"
```
@@ -146,7 +146,7 @@ To install Toolbox as a binary on Windows (PowerShell):
```powershell
# see releases page for other versions
$VERSION = "0.25.0"
$VERSION = "0.24.0"
curl.exe -o toolbox.exe "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe"
```
@@ -158,7 +158,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.25.0
export VERSION=0.24.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -177,7 +177,7 @@ To install from source, ensure you have the latest version of
[Go installed](https://go.dev/doc/install), and then run the following command:
```sh
go install github.com/googleapis/genai-toolbox@v0.25.0
go install github.com/googleapis/genai-toolbox@v0.24.0
```
{{% /tab %}}

View File

@@ -105,7 +105,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -13,7 +13,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -100,19 +100,19 @@ After you install Looker in the MCP Store, resources and tools from the server a
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -45,19 +45,19 @@ instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -43,19 +43,19 @@ expose your developer assistant tools to a MySQL instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -44,19 +44,19 @@ expose your developer assistant tools to a Neo4j instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -56,19 +56,19 @@ Omni](https://cloud.google.com/alloydb/omni/current/docs/overview).
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -43,19 +43,19 @@ to expose your developer assistant tools to a SQLite instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -68,12 +68,7 @@ networks:
```
{{< notice tip >}}
To prevent DNS rebinding attack, use the `--allowed-hosts` flag to specify a
list of hosts for validation. E.g. `command: [ "toolbox",
"--tools-file", "/config/tools.yaml", "--address", "0.0.0.0",
"--allowed-hosts", "localhost:5000"]`
To implement CORs, use the `--allowed-origins` flag to specify a
To prevent DNS rebinding attack, use the `--allowed-origins` flag to specify a
list of origins permitted to access the server. E.g. `command: [ "toolbox",
"--tools-file", "/config/tools.yaml", "--address", "0.0.0.0",
"--allowed-origins", "https://foo.bar"]`

View File

@@ -188,13 +188,9 @@ description: >
path: tools.yaml
```
{{< notice tip >}}
{{< notice tip >}}
To prevent DNS rebinding attack, use the `--allowed-origins` flag to specify a
list of origins permitted to access the server. E.g. `args: ["--address",
"0.0.0.0", "--allowed-hosts", "foo.bar:5000"]`
To implement CORs, use the `--allowed-origins` flag to specify a
list of origins permitted to access the server. E.g. `args: ["--address",
"0.0.0.0", "--allowed-origins", "https://foo.bar"]`
{{< /notice >}}

View File

@@ -142,18 +142,14 @@ deployment will time out.
### Update deployed server to be secure
To prevent DNS rebinding attack, use the `--allowed-hosts` flag to specify a
list of hosts. In order to do that, you will
To prevent DNS rebinding attack, use the `--allowed-origins` flag to specify a
list of origins permitted to access the server. In order to do that, you will
have to re-deploy the cloud run service with the new flag.
To implement CORs checks, use the `--allowed-origins` flag to specify a list of
origins permitted to access the server.
1. Set an environment variable to the cloud run url:
```bash
export URL=<cloud run url>
export HOST=<cloud run host>
```
2. Redeploy Toolbox:
@@ -164,7 +160,7 @@ origins permitted to access the server.
--service-account toolbox-identity \
--region us-central1 \
--set-secrets "/app/tools.yaml=tools:latest" \
--args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080","--allowed-origins=$URL","--allowed-hosts=$HOST"
--args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080","--allowed-origins=$URL"
# --allow-unauthenticated # https://cloud.google.com/run/docs/authenticating/public#gcloud
```
@@ -176,7 +172,7 @@ origins permitted to access the server.
--service-account toolbox-identity \
--region us-central1 \
--set-secrets "/app/tools.yaml=tools:latest" \
--args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080","--allowed-origins=$URL","--allowed-hosts=$HOST" \
--args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080","--allowed-origins=$URL" \
# TODO(dev): update the following to match your VPC if necessary
--network default \
--subnet default

View File

@@ -8,26 +8,25 @@ description: >
## Reference
| Flag (Short) | Flag (Long) | Description | Default |
|--------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| `-a` | `--address` | Address of the interface the server will listen on. | `127.0.0.1` |
| | `--disable-reload` | Disables dynamic reloading of tools file. | |
| `-h` | `--help` | help for toolbox | |
| | `--log-level` | Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'. | `info` |
| | `--logging-format` | Specify logging format to use. Allowed: 'standard' or 'JSON'. | `standard` |
| `-p` | `--port` | Port the server will listen on. | `5000` |
| | `--prebuilt` | Use a prebuilt tool configuration by source type. See [Prebuilt Tools Reference](prebuilt-tools.md) for allowed values. | |
| | `--stdio` | Listens via MCP STDIO instead of acting as a remote HTTP server. | |
| | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | |
| | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | |
| | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` |
| Flag (Short) | Flag (Long) | Description | Default |
|--------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| `-a` | `--address` | Address of the interface the server will listen on. | `127.0.0.1` |
| | `--disable-reload` | Disables dynamic reloading of tools file. | |
| `-h` | `--help` | help for toolbox | |
| | `--log-level` | Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'. | `info` |
| | `--logging-format` | Specify logging format to use. Allowed: 'standard' or 'JSON'. | `standard` |
| `-p` | `--port` | Port the server will listen on. | `5000` |
| | `--prebuilt` | Use a prebuilt tool configuration by source type. See [Prebuilt Tools Reference](prebuilt-tools.md) for allowed values. | |
| | `--stdio` | Listens via MCP STDIO instead of acting as a remote HTTP server. | |
| | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | |
| | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | |
| | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` |
| | `--tools-file` | File path specifying the tool configuration. Cannot be used with --tools-files or --tools-folder. | |
| | `--tools-files` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --tools-file or --tools-folder. | |
| | `--tools-folder` | Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --tools-file or --tools-files. | |
| | `--ui` | Launches the Toolbox UI web server. | |
| | `--allowed-origins` | Specifies a list of origins permitted to access this server for CORs access. | `*` |
| | `--allowed-hosts` | Specifies a list of hosts permitted to access this server to prevent DNS rebinding attacks. | `*` |
| `-v` | `--version` | version for toolbox | |
| | `--ui` | Launches the Toolbox UI web server. | |
| | `--allowed-origins` | Specifies a list of origins permitted to access this server. | `*` |
| `-v` | `--version` | version for toolbox | |
## Examples

View File

@@ -1,42 +0,0 @@
---
title: Spanner Admin
type: docs
weight: 1
description: "A \"spanner-admin\" source provides a client for the Cloud Spanner Admin API.\n"
alias: [/resources/sources/spanner-admin]
---
## About
The `spanner-admin` source provides a client to interact with the [Google
Cloud Spanner Admin API](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1). This
allows tools to perform administrative tasks on Spanner instances, such as
creating instances.
Authentication can be handled in two ways:
1. **Application Default Credentials (ADC):** By default, the source uses ADC
to authenticate with the API.
2. **Client-side OAuth:** If `useClientOAuth` is set to `true`, the source will
expect an OAuth 2.0 access token to be provided by the client (e.g., a web
browser) for each request.
## Example
```yaml
sources:
my-spanner-admin:
kind: spanner-admin
my-oauth-spanner-admin:
kind: spanner-admin
useClientOAuth: true
```
## Reference
| **field** | **type** | **required** | **description** |
| -------------- | :------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| kind | string | true | Must be "spanner-admin". |
| defaultProject | string | false | The Google Cloud project ID to use for Spanner infrastructure tools. |
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |

View File

@@ -1,52 +0,0 @@
---
title: spanner-create-instance
type: docs
weight: 2
description: "Create a Cloud Spanner instance."
---
The `spanner-create-instance` tool creates a new Cloud Spanner instance in a
specified Google Cloud project.
{{< notice info >}}
This tool uses the `spanner-admin` source.
{{< /notice >}}
## Configuration
Here is an example of how to configure the `spanner-create-instance` tool in
your `tools.yaml` file:
```yaml
sources:
my-spanner-admin-source:
kind: spanner-admin
tools:
create_my_spanner_instance:
kind: spanner-create-instance
source: my-spanner-admin-source
description: "Creates a Spanner instance."
```
## Parameters
The `spanner-create-instance` tool has the following parameters:
| **field** | **type** | **required** | **description** |
| --------------- | :------: | :----------: | ------------------------------------------------------------------------------------ |
| project | string | true | The Google Cloud project ID. |
| instanceId | string | true | The ID of the instance to create. |
| displayName | string | true | The display name of the instance. |
| config | string | true | The instance configuration (e.g., `regional-us-central1`). |
| nodeCount | integer | true | The number of nodes. Mutually exclusive with `processingUnits` (one must be 0). |
| processingUnits | integer | true | The number of processing units. Mutually exclusive with `nodeCount` (one must be 0). |
| edition | string | false | The edition of the instance (`STANDARD`, `ENTERPRISE`, `ENTERPRISE_PLUS`). |
## Reference
| **field** | **type** | **required** | **description** |
| ----------- | :------: | :----------: | ------------------------------------------------------------ |
| kind | string | true | Must be `spanner-create-instance`. |
| source | string | true | The name of the `spanner-admin` source to use for this tool. |
| description | string | false | A description of the tool that is passed to the agent. |

View File

@@ -771,7 +771,7 @@
},
"outputs": [],
"source": [
"version = \"0.25.0\" # x-release-please-version\n",
"version = \"0.24.0\" # x-release-please-version\n",
"! curl -L -o /content/toolbox https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -123,7 +123,7 @@ In this section, we will download and install the Toolbox binary.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
export VERSION="0.25.0"
export VERSION="0.24.0"
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -220,7 +220,7 @@
},
"outputs": [],
"source": [
"version = \"0.25.0\" # x-release-please-version\n",
"version = \"0.24.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -179,7 +179,7 @@ to use BigQuery, and then run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -98,7 +98,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -48,7 +48,7 @@ In this section, we will download Toolbox and run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.25.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.24.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -1,6 +1,6 @@
{
"name": "mcp-toolbox-for-databases",
"version": "0.25.0",
"version": "0.24.0",
"description": "MCP Toolbox for Databases is an open-source MCP server for more than 30 different datasources.",
"contextFileName": "MCP-TOOLBOX-EXTENSION.md"
}

View File

@@ -50,7 +50,6 @@ var expectedToolSources = []string{
"serverless-spark",
"singlestore",
"snowflake",
"spanner-admin",
"spanner-postgres",
"spanner",
"sqlite",

View File

@@ -1,27 +0,0 @@
# 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.
sources:
spanner-admin-source:
kind: spanner-admin
defaultProject: ${SPANNER_PROJECT:}
tools:
create_instance:
kind: spanner-create-instance
source: spanner-admin-source
toolsets:
spanner_admin_tools:
- create_instance

View File

@@ -68,8 +68,6 @@ type ServerConfig struct {
UI bool
// Specifies a list of origins permitted to access this server.
AllowedOrigins []string
// Specifies a list of hosts permitted to access this server
AllowedHosts []string
}
type logFormat string

View File

@@ -300,21 +300,6 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
return sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, nil
}
func hostCheck(allowedHosts map[string]struct{}) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, hasWildcard := allowedHosts["*"]
_, hostIsAllowed := allowedHosts[r.Host]
if !hasWildcard && !hostIsAllowed {
// Return 400 Bad Request or 403 Forbidden to block the attack
http.Error(w, "Invalid Host header", http.StatusBadRequest)
return
}
next.ServeHTTP(w, r)
})
}
}
// NewServer returns a Server object based on provided Config.
func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
instrumentation, err := util.InstrumentationFromContext(ctx)
@@ -389,7 +374,7 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
// cors
if slices.Contains(cfg.AllowedOrigins, "*") {
s.logger.WarnContext(ctx, "wildcard (`*`) allows all origin to access the resource and is not secure. Use it with cautious for public, non-sensitive data, or during local development. Recommended to use `--allowed-origins` flag")
s.logger.WarnContext(ctx, "wildcard (`*`) allows all origin to access the resource and is not secure. Use it with cautious for public, non-sensitive data, or during local development. Recommended to use `--allowed-origins` flag to prevent DNS rebinding attacks")
}
corsOpts := cors.Options{
AllowedOrigins: cfg.AllowedOrigins,
@@ -400,15 +385,6 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
MaxAge: 300, // cache preflight results for 5 minutes
}
r.Use(cors.Handler(corsOpts))
// validate hosts for DNS rebinding attacks
if slices.Contains(cfg.AllowedHosts, "*") {
s.logger.WarnContext(ctx, "wildcard (`*`) allows all hosts to access the resource and is not secure. Use it with cautious for public, non-sensitive data, or during local development. Recommended to use `--allowed-hosts` flag to prevent DNS rebinding attacks")
}
allowedHostsMap := make(map[string]struct{}, len(cfg.AllowedHosts))
for _, h := range cfg.AllowedHosts {
allowedHostsMap[h] = struct{}{}
}
r.Use(hostCheck(allowedHostsMap))
// control plane
apiR, err := apiRouter(s)

View File

@@ -43,10 +43,9 @@ func TestServe(t *testing.T) {
addr, port := "127.0.0.1", 5000
cfg := server.ServerConfig{
Version: "0.0.0",
Address: addr,
Port: port,
AllowedHosts: []string{"*"},
Version: "0.0.0",
Address: addr,
Port: port,
}
otelShutdown, err := telemetry.SetupOTel(ctx, "0.0.0", "", false, "toolbox")

View File

@@ -15,7 +15,9 @@ package looker
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"strings"
"time"
@@ -208,6 +210,49 @@ func (s *Source) LookerSessionLength() int64 {
return s.SessionLength
}
// Make types for RoundTripper
type transportWithAuthHeader struct {
Base http.RoundTripper
AuthToken string
}
func (t *transportWithAuthHeader) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("x-looker-appid", "go-sdk")
req.Header.Set("Authorization", t.AuthToken)
return t.Base.RoundTrip(req)
}
func (s *Source) GetLookerSDK(accessToken string) (*v4.LookerSDK, error) {
if s.UseClientAuthorization() {
if accessToken == "" {
return nil, fmt.Errorf("no access token supplied with request")
}
session := rtl.NewAuthSession(*s.LookerApiSettings())
// Configure base transport with TLS
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: !s.LookerApiSettings().VerifySsl,
},
}
// Build transport for end user token
session.Client = http.Client{
Transport: &transportWithAuthHeader{
Base: transport,
AuthToken: accessToken,
},
}
// return SDK with new Transport
return v4.NewLookerSDK(session), nil
}
if s.LookerClient() == nil {
return nil, fmt.Errorf("client id or client secret not valid")
}
return s.LookerClient(), nil
}
func initGoogleCloudConnection(ctx context.Context) (oauth2.TokenSource, error) {
cred, err := google.FindDefaultCredentials(ctx, geminidataanalytics.DefaultAuthScopes()...)
if err != nil {

View File

@@ -1,120 +0,0 @@
// 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 spanneradmin
import (
"context"
"fmt"
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
"github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/util"
"go.opentelemetry.io/otel/trace"
"golang.org/x/oauth2"
"google.golang.org/api/option"
)
const SourceKind string = "spanner-admin"
// validate interface
var _ sources.SourceConfig = Config{}
func init() {
if !sources.Register(SourceKind, newConfig) {
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
}
}
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
DefaultProject string `yaml:"defaultProject"`
UseClientOAuth bool `yaml:"useClientOAuth"`
}
func (r Config) SourceConfigKind() string {
return SourceKind
}
// Initialize initializes a Spanner Admin Source instance.
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
var client *instance.InstanceAdminClient
if !r.UseClientOAuth {
ua, err := util.UserAgentFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("error in User Agent retrieval: %s", err)
}
// Use Application Default Credentials
client, err = instance.NewInstanceAdminClient(ctx, option.WithUserAgent(ua))
if err != nil {
return nil, fmt.Errorf("error creating new spanner instance admin client: %w", err)
}
}
s := &Source{
Config: r,
Client: client,
}
return s, nil
}
var _ sources.Source = &Source{}
type Source struct {
Config
Client *instance.InstanceAdminClient
}
func (s *Source) SourceKind() string {
return SourceKind
}
func (s *Source) ToConfig() sources.SourceConfig {
return s.Config
}
func (s *Source) GetDefaultProject() string {
return s.DefaultProject
}
func (s *Source) GetClient(ctx context.Context, accessToken string) (*instance.InstanceAdminClient, error) {
if s.UseClientOAuth {
token := &oauth2.Token{AccessToken: accessToken}
ua, err := util.UserAgentFromContext(ctx)
if err != nil {
return nil, err
}
client, err := instance.NewInstanceAdminClient(ctx, option.WithTokenSource(oauth2.StaticTokenSource(token)), option.WithUserAgent(ua))
if err != nil {
return nil, fmt.Errorf("error creating new spanner instance admin client: %w", err)
}
return client, nil
}
return s.Client, nil
}
func (s *Source) UseClientAuthorization() bool {
return s.UseClientOAuth
}

View File

@@ -1,135 +0,0 @@
// 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 spanneradmin_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/sources/spanneradmin"
"github.com/googleapis/genai-toolbox/internal/testutils"
)
func TestParseFromYamlSpannerAdmin(t *testing.T) {
t.Parallel()
tcs := []struct {
desc string
in string
want server.SourceConfigs
}{
{
desc: "basic example",
in: `
sources:
my-spanner-admin-instance:
kind: spanner-admin
`,
want: map[string]sources.SourceConfig{
"my-spanner-admin-instance": spanneradmin.Config{
Name: "my-spanner-admin-instance",
Kind: spanneradmin.SourceKind,
UseClientOAuth: false,
},
},
},
{
desc: "use client auth example",
in: `
sources:
my-spanner-admin-instance:
kind: spanner-admin
useClientOAuth: true
`,
want: map[string]sources.SourceConfig{
"my-spanner-admin-instance": spanneradmin.Config{
Name: "my-spanner-admin-instance",
Kind: spanneradmin.SourceKind,
UseClientOAuth: true,
},
},
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
got := struct {
Sources server.SourceConfigs `yaml:"sources"`
}{}
// Parse contents
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if !cmp.Equal(tc.want, got.Sources) {
t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources)
}
})
}
}
func TestFailParseFromYaml(t *testing.T) {
t.Parallel()
tcs := []struct {
desc string
in string
err string
}{
{
desc: "extra field",
in: `
sources:
my-spanner-admin-instance:
kind: spanner-admin
project: test-project
`,
err: `unable to parse source "my-spanner-admin-instance" as "spanner-admin": [2:1] unknown field "project"
1 | kind: spanner-admin
> 2 | project: test-project
^
`,
},
{
desc: "missing required field",
in: `
sources:
my-spanner-admin-instance:
useClientOAuth: true
`,
err: "missing 'kind' field for source \"my-spanner-admin-instance\"",
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
got := struct {
Sources server.SourceConfigs `yaml:"sources"`
}{}
// Parse contents
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
if err == nil {
t.Fatalf("expect parsing to fail")
}
errStr := err.Error()
if errStr != tc.err {
t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err)
}
})
}
}

View File

@@ -77,21 +77,26 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
placeholderParts = append(placeholderParts, fmt.Sprintf("$%d", i+3)) // $1, $2 reserved
}
var stmt string
var paramNamesSQL string
var paramValuesSQL string
if numParams > 0 {
paramNamesSQL := fmt.Sprintf("ARRAY[%s]", strings.Join(quotedNameParts, ", "))
paramValuesSQL := fmt.Sprintf("ARRAY[%s]", strings.Join(placeholderParts, ", "))
// execute_nl_query is the AlloyDB AI function that executes the natural language query
// The first parameter is the natural language query, which is passed as $1
// The second parameter is the NLConfig, which is passed as a $2
// The following params are the list of PSV values passed to the NLConfig
// Example SQL statement being executed:
// SELECT alloydb_ai_nl.execute_nl_query(nl_question => 'How many tickets do I have?', nl_config_id => 'cymbal_air_nl_config', param_names => ARRAY ['user_email'], param_values => ARRAY ['hailongli@google.com']);
stmtFormat := "SELECT alloydb_ai_nl.execute_nl_query(nl_question => $1, nl_config_id => $2, param_names => %s, param_values => %s);"
stmt = fmt.Sprintf(stmtFormat, paramNamesSQL, paramValuesSQL)
paramNamesSQL = fmt.Sprintf("ARRAY[%s]", strings.Join(quotedNameParts, ", "))
paramValuesSQL = fmt.Sprintf("ARRAY[%s]", strings.Join(placeholderParts, ", "))
} else {
stmt = "SELECT alloydb_ai_nl.execute_nl_query(nl_question => $1, nl_config_id => $2);"
paramNamesSQL = "ARRAY[]::TEXT[]"
paramValuesSQL = "ARRAY[]::TEXT[]"
}
// execute_nl_query is the AlloyDB AI function that executes the natural language query
// The first parameter is the natural language query, which is passed as $1
// The second parameter is the NLConfig, which is passed as a $2
// The following params are the list of PSV values passed to the NLConfig
// Example SQL statement being executed:
// SELECT alloydb_ai_nl.execute_nl_query(nl_question => 'How many tickets do I have?', nl_config_id => 'cymbal_air_nl_config', param_names => ARRAY ['user_email'], param_values => ARRAY ['hailongli@google.com']);
stmtFormat := "SELECT alloydb_ai_nl.execute_nl_query(nl_question => $1, nl_config_id => $2, param_names => %s, param_values => %s);"
stmt := fmt.Sprintf(stmtFormat, paramNamesSQL, paramValuesSQL)
newQuestionParam := parameters.NewStringParameter(
"question", // name
"The natural language question to ask.", // description

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -159,7 +159,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
visConfig := paramsMap["vis_config"].(map[string]any)
wq.VisConfig = &visConfig
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -192,7 +191,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
req.Dimension = &dimension
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -15,65 +15,17 @@ package lookercommon
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
rtl "github.com/looker-open-source/sdk-codegen/go/rtl"
"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
"github.com/thlib/go-timezone-local/tzlocal"
)
// Make types for RoundTripper
type transportWithAuthHeader struct {
Base http.RoundTripper
AuthToken tools.AccessToken
}
func (t *transportWithAuthHeader) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("x-looker-appid", "go-sdk")
req.Header.Set("Authorization", string(t.AuthToken))
return t.Base.RoundTrip(req)
}
func GetLookerSDK(useClientOAuth bool, config *rtl.ApiSettings, client *v4.LookerSDK, accessToken tools.AccessToken) (*v4.LookerSDK, error) {
if useClientOAuth {
if accessToken == "" {
return nil, fmt.Errorf("no access token supplied with request")
}
session := rtl.NewAuthSession(*config)
// Configure base transport with TLS
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: !config.VerifySsl,
},
}
// Build transport for end user token
session.Client = http.Client{
Transport: &transportWithAuthHeader{
Base: transport,
AuthToken: accessToken,
},
}
// return SDK with new Transport
return v4.NewLookerSDK(session), nil
}
if client == nil {
return nil, fmt.Errorf("client id or client secret not valid")
}
return client, nil
}
const (
DimensionsFields = "fields(dimensions(name,type,label,label_short,description,synonyms,tags,hidden,suggestable,suggestions,suggest_dimension,suggest_explore))"
FiltersFields = "fields(filters(name,type,label,label_short,description,synonyms,tags,hidden,suggestable,suggestions,suggest_dimension,suggest_explore))"

View File

@@ -47,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -116,7 +116,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, err
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -47,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -117,7 +117,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, err
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -125,7 +124,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("'devMode' must be a boolean, got %T", mapParams["devMode"])
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -22,7 +22,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -49,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerSessionLength() int64
}
@@ -137,7 +136,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
contentId_ptr = nil
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/looker-open-source/sdk-codegen/go/rtl"
@@ -47,8 +46,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -120,7 +119,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("'conn' must be a string, got %T", mapParams["conn"])
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -119,7 +118,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/looker-open-source/sdk-codegen/go/rtl"
@@ -47,8 +46,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -122,7 +121,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
db, _ := mapParams["db"].(string)
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -137,7 +136,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("'tables' must be a string, got %T", mapParams["tables"])
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -132,7 +131,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("'schema' must be a string, got %T", mapParams["schema"])
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -141,7 +140,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
limit := int64(paramsMap["limit"].(int))
offset := int64(paramsMap["offset"].(int))
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerShowHiddenFields() bool
}
@@ -124,7 +124,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("error processing model or explore: %w", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerShowHiddenExplores() bool
}
@@ -126,7 +125,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("'model' must be a string, got %T", mapParams["model"])
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerShowHiddenFields() bool
}
@@ -125,7 +125,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
fields := lookercommon.FiltersFields
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -141,7 +140,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
limit := int64(paramsMap["limit"].(int))
offset := int64(paramsMap["offset"].(int))
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerShowHiddenFields() bool
}
@@ -125,7 +125,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
fields := lookercommon.MeasuresFields
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerShowHiddenModels() bool
}
@@ -124,7 +123,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
excludeHidden := !source.LookerShowHiddenModels()
includeInternal := true
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
LookerShowHiddenFields() bool
}
@@ -125,7 +125,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
fields := lookercommon.ParametersFields
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -121,7 +121,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -120,7 +119,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -48,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -119,7 +118,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -53,8 +53,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -136,7 +136,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -53,8 +53,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -127,7 +127,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -53,8 +53,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -131,7 +131,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, err
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -23,7 +23,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
@@ -50,8 +49,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -129,7 +128,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
logger.DebugContext(ctx, "params = ", params)
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -50,8 +50,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -139,7 +139,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("error building query request: %w", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -49,8 +49,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -123,7 +123,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
if err != nil {
return nil, fmt.Errorf("error building WriteQuery request: %w", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -122,7 +122,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
if err != nil {
return nil, fmt.Errorf("error building query request: %w", err)
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -48,8 +48,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -135,7 +135,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
visConfig := paramsMap["vis_config"].(map[string]any)
wq.VisConfig = &visConfig
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -50,8 +50,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -129,7 +129,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
dashboard_id := paramsMap["dashboard_id"].(string)
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -49,8 +49,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
@@ -132,7 +132,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
limit := int64(paramsMap["limit"].(int))
limitStr := fmt.Sprintf("%d", limit)
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -47,8 +47,8 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
type compatibleSource interface {
UseClientAuthorization() bool
GetAuthTokenHeaderName() string
LookerClient() *v4.LookerSDK
LookerApiSettings() *rtl.ApiSettings
GetLookerSDK(string) (*v4.LookerSDK, error)
}
type Config struct {
Name string `yaml:"name" validate:"required"`
@@ -117,7 +117,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, err
}
sdk, err := lookercommon.GetLookerSDK(source.UseClientAuthorization(), source.LookerApiSettings(), source.LookerClient(), accessToken)
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}

View File

@@ -1,233 +0,0 @@
// 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 spannercreateinstance
import (
"context"
"fmt"
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
"cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
"github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
const kind string = "spanner-create-instance"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}
type compatibleSource interface {
GetDefaultProject() string
GetClient(context.Context, string) (*instance.InstanceAdminClient, error)
UseClientAuthorization() bool
}
// Config defines the configuration for the create-instance tool.
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Description string `yaml:"description"`
Source string `yaml:"source" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
// ToolConfigKind returns the kind of the tool.
func (cfg Config) ToolConfigKind() string {
return kind
}
// Initialize initializes the tool from the configuration.
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source %q not compatible", kind, cfg.Source)
}
project := s.GetDefaultProject()
var projectParam parameters.Parameter
if project != "" {
projectParam = parameters.NewStringParameterWithDefault("project", project, "The GCP project ID.")
} else {
projectParam = parameters.NewStringParameter("project", "The project ID")
}
allParameters := parameters.Parameters{
projectParam,
parameters.NewStringParameter("instanceId", "The ID of the instance"),
parameters.NewStringParameter("displayName", "The display name of the instance"),
parameters.NewStringParameter("config", "The instance configuration (e.g., regional-us-central1)"),
parameters.NewIntParameter("nodeCount", "The number of nodes, mutually exclusive with processingUnits (one must be 0)"),
parameters.NewIntParameter("processingUnits", "The number of processing units, mutually exclusive with nodeCount (one must be 0)"),
parameters.NewStringParameter("edition", "The edition of the instance (STANDARD, ENTERPRISE, ENTERPRISE_PLUS)"),
}
paramManifest := allParameters.Manifest()
description := cfg.Description
if description == "" {
description = "Creates a Spanner instance."
}
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters, nil)
return Tool{
Config: cfg,
AllParams: allParameters,
manifest: tools.Manifest{Description: description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}, nil
}
// Tool represents the create-instance tool.
type Tool struct {
Config
AllParams parameters.Parameters
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) ToConfig() tools.ToolConfig {
return t.Config
}
// Invoke executes the tool's logic.
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
paramsMap := params.AsMap()
project, _ := paramsMap["project"].(string)
instanceId, _ := paramsMap["instanceId"].(string)
displayName, _ := paramsMap["displayName"].(string)
config, _ := paramsMap["config"].(string)
nodeCount, _ := paramsMap["nodeCount"].(int)
processingUnits, _ := paramsMap["processingUnits"].(int)
editionStr, _ := paramsMap["edition"].(string)
if (nodeCount > 0 && processingUnits > 0) || (nodeCount == 0 && processingUnits == 0) {
return nil, fmt.Errorf("one of nodeCount or processingUnits must be positive, and the other must be 0")
}
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Kind)
if err != nil {
return nil, err
}
client, err := source.GetClient(ctx, string(accessToken))
if err != nil {
return nil, err
}
if source.UseClientAuthorization() {
defer client.Close()
}
parent := fmt.Sprintf("projects/%s", project)
instanceConfig := fmt.Sprintf("projects/%s/instanceConfigs/%s", project, config)
var edition instancepb.Instance_Edition
switch editionStr {
case "STANDARD":
edition = instancepb.Instance_STANDARD
case "ENTERPRISE":
edition = instancepb.Instance_ENTERPRISE
case "ENTERPRISE_PLUS":
edition = instancepb.Instance_ENTERPRISE_PLUS
default:
edition = instancepb.Instance_EDITION_UNSPECIFIED
}
// Construct the instance object
instance := &instancepb.Instance{
Config: instanceConfig,
DisplayName: displayName,
Edition: edition,
NodeCount: int32(nodeCount),
ProcessingUnits: int32(processingUnits),
}
req := &instancepb.CreateInstanceRequest{
Parent: parent,
InstanceId: instanceId,
Instance: instance,
}
op, err := client.CreateInstance(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to create instance: %w", err)
}
// Wait for the operation to complete
resp, err := op.Wait(ctx)
if err != nil {
return nil, fmt.Errorf("failed to wait for create instance operation: %w", err)
}
return resp, nil
}
// ParseParams parses the parameters for the tool.
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (parameters.ParamValues, error) {
return parameters.ParseParams(t.AllParams, data, claims)
}
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)
}
// Manifest returns the tool's manifest.
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
// McpManifest returns the tool's MCP manifest.
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
// Authorized checks if the tool is authorized.
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return true
}
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Kind)
if err != nil {
return false, err
}
return source.UseClientAuthorization(), nil
}
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
return "Authorization", nil
}

View File

@@ -1,110 +0,0 @@
// 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 spannercreateinstance_test
import (
"context"
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/spanneradmin/spannercreateinstance"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
func TestParseFromYaml(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
tcs := []struct {
desc string
in string
want server.ToolConfigs
}{
{
desc: "basic example",
in: `
tools:
create-instance-tool:
kind: spanner-create-instance
description: a test description
source: a-source
`,
want: server.ToolConfigs{
"create-instance-tool": spannercreateinstance.Config{
Name: "create-instance-tool",
Kind: "spanner-create-instance",
Description: "a test description",
Source: "a-source",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestInvokeNodeCountAndProcessingUnitsValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
params parameters.ParamValues
wantErr string
}{
{
name: "Both positive",
params: parameters.ParamValues{
{Name: "nodeCount", Value: 1},
{Name: "processingUnits", Value: 1000},
},
wantErr: "one of nodeCount or processingUnits must be positive, and the other must be 0",
},
{
name: "Both zero",
params: parameters.ParamValues{
{Name: "nodeCount", Value: 0},
{Name: "processingUnits", Value: 0},
},
wantErr: "one of nodeCount or processingUnits must be positive, and the other must be 0",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tool := spannercreateinstance.Tool{}
_, err := tool.Invoke(context.Background(), nil, tc.params, "")
if err == nil || err.Error() != tc.wantErr {
t.Errorf("Invoke() error = %v, wantErr %v", err, tc.wantErr)
}
})
}
}

View File

@@ -478,13 +478,9 @@ func (ps Parameters) McpManifest() (McpToolsSchema, map[string][]string) {
for _, p := range ps {
name := p.GetName()
paramManifest, authParamList := p.McpManifest()
defaultV := p.GetDefault()
if defaultV != nil {
paramManifest.Default = defaultV
}
properties[name] = paramManifest
// parameters that doesn't have a default value are added to the required field
if CheckParamRequired(p.GetRequired(), defaultV) {
if CheckParamRequired(p.GetRequired(), p.GetDefault()) {
required = append(required, name)
}
if len(authParamList) > 0 {
@@ -506,7 +502,6 @@ type ParameterManifest struct {
Description string `json:"description"`
AuthServices []string `json:"authSources"`
Items *ParameterManifest `json:"items,omitempty"`
Default any `json:"default,omitempty"`
AdditionalProperties any `json:"additionalProperties,omitempty"`
EmbeddedBy string `json:"embeddedBy,omitempty"`
}
@@ -516,7 +511,6 @@ type ParameterMcpManifest struct {
Type string `json:"type"`
Description string `json:"description"`
Items *ParameterMcpManifest `json:"items,omitempty"`
Default any `json:"default,omitempty"`
AdditionalProperties any `json:"additionalProperties,omitempty"`
}
@@ -794,7 +788,6 @@ func (p *StringParameter) Manifest() ParameterManifest {
Required: r,
Description: p.Desc,
AuthServices: authServiceNames,
Default: p.GetDefault(),
}
}
@@ -953,7 +946,6 @@ func (p *IntParameter) Manifest() ParameterManifest {
Required: r,
Description: p.Desc,
AuthServices: authServiceNames,
Default: p.GetDefault(),
}
}
@@ -1110,7 +1102,6 @@ func (p *FloatParameter) Manifest() ParameterManifest {
Required: r,
Description: p.Desc,
AuthServices: authServiceNames,
Default: p.GetDefault(),
}
}
@@ -1244,7 +1235,6 @@ func (p *BooleanParameter) Manifest() ParameterManifest {
Required: r,
Description: p.Desc,
AuthServices: authServiceNames,
Default: p.GetDefault(),
}
}
@@ -1440,7 +1430,6 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
Description: p.Desc,
AuthServices: authServiceNames,
Items: &items,
Default: p.GetDefault(),
}
}
@@ -1686,10 +1675,7 @@ func (p *MapParameter) Manifest() ParameterManifest {
// If no valueType is given, allow any properties.
additionalProperties = true
}
var defaultV any
if p.Default != nil {
defaultV = *p.Default
}
return ParameterManifest{
Name: p.Name,
Type: "object",
@@ -1697,7 +1683,6 @@ func (p *MapParameter) Manifest() ParameterManifest {
Description: p.Desc,
AuthServices: authServiceNames,
AdditionalProperties: additionalProperties,
Default: defaultV,
}
}

View File

@@ -1625,22 +1625,22 @@ func TestParamManifest(t *testing.T) {
{
name: "string default",
in: parameters.NewStringParameterWithDefault("foo-string", "foo", "bar"),
want: parameters.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", Default: "foo", AuthServices: []string{}},
want: parameters.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "int default",
in: parameters.NewIntParameterWithDefault("foo-int", 1, "bar"),
want: parameters.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", Default: 1, AuthServices: []string{}},
want: parameters.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "float default",
in: parameters.NewFloatParameterWithDefault("foo-float", 1.1, "bar"),
want: parameters.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", Default: 1.1, AuthServices: []string{}},
want: parameters.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "boolean default",
in: parameters.NewBooleanParameterWithDefault("foo-bool", true, "bar"),
want: parameters.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", Default: true, AuthServices: []string{}},
want: parameters.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "array default",
@@ -1650,7 +1650,6 @@ func TestParamManifest(t *testing.T) {
Type: "array",
Required: false,
Description: "bar",
Default: []any{"foo", "bar"},
AuthServices: []string{},
Items: &parameters.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
@@ -1842,7 +1841,7 @@ func TestMcpManifest(t *testing.T) {
wantSchema: parameters.McpToolsSchema{
Type: "object",
Properties: map[string]parameters.ParameterMcpManifest{
"foo-string": {Type: "string", Description: "bar", Default: "foo"},
"foo-string": {Type: "string", Description: "bar"},
"foo-string2": {Type: "string", Description: "bar"},
"foo-string3-auth": {Type: "string", Description: "bar"},
"foo-int2": {Type: "integer", Description: "bar"},

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
"name": "io.github.googleapis/genai-toolbox",
"description": "MCP Toolbox for Databases enables your agent to connect to your database.",
"title": "MCP Toolbox",
@@ -14,11 +14,11 @@
"url": "https://github.com/googleapis/genai-toolbox",
"source": "github"
},
"version": "0.25.0",
"version": "0.24.0",
"packages": [
{
"registryType": "oci",
"identifier": "us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:0.25.0",
"identifier": "us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:0.24.0",
"transport": {
"type": "streamable-http",
"url": "http://{host}:{port}/mcp"

View File

@@ -194,7 +194,7 @@ func runAlloyDBToolGetTest(t *testing.T) {
"description": "Simple tool to test end to end functionality.",
"parameters": []any{
map[string]any{"name": "project", "type": "string", "description": "The GCP project ID to list clusters for.", "required": true, "authSources": []any{}},
map[string]any{"name": "location", "type": "string", "description": "Optional: The location to list clusters in (e.g., 'us-central1'). Use '-' to list clusters across all locations.(Default: '-')", "required": false, "default": "-", "authSources": []any{}},
map[string]any{"name": "location", "type": "string", "description": "Optional: The location to list clusters in (e.g., 'us-central1'). Use '-' to list clusters across all locations.(Default: '-')", "required": false, "authSources": []any{}},
},
"authRequired": []any{},
},

View File

@@ -431,7 +431,6 @@ func TestLooker(t *testing.T) {
"description": "The filters for the query",
"name": "filters",
"required": false,
"default": map[string]any{},
"type": "object",
},
map[string]any{
@@ -447,7 +446,6 @@ func TestLooker(t *testing.T) {
"name": "pivots",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -462,7 +460,6 @@ func TestLooker(t *testing.T) {
"name": "sorts",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -470,7 +467,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(500),
},
map[string]any{
"authSources": []any{},
@@ -523,7 +519,6 @@ func TestLooker(t *testing.T) {
"description": "The filters for the query",
"name": "filters",
"required": false,
"default": map[string]any{},
"type": "object",
},
map[string]any{
@@ -539,7 +534,6 @@ func TestLooker(t *testing.T) {
"name": "pivots",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -554,7 +548,6 @@ func TestLooker(t *testing.T) {
"name": "sorts",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -562,7 +555,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(500),
},
map[string]any{
"authSources": []any{},
@@ -615,7 +607,6 @@ func TestLooker(t *testing.T) {
"description": "The filters for the query",
"name": "filters",
"required": false,
"default": map[string]any{},
"type": "object",
},
map[string]any{
@@ -631,7 +622,6 @@ func TestLooker(t *testing.T) {
"name": "pivots",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -646,7 +636,6 @@ func TestLooker(t *testing.T) {
"name": "sorts",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -654,7 +643,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(500),
},
map[string]any{
"authSources": []any{},
@@ -670,7 +658,6 @@ func TestLooker(t *testing.T) {
"name": "vis_config",
"required": false,
"type": "object",
"default": map[string]any{},
},
},
},
@@ -688,7 +675,6 @@ func TestLooker(t *testing.T) {
"name": "title",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -696,7 +682,6 @@ func TestLooker(t *testing.T) {
"name": "desc",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -704,7 +689,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(100),
},
map[string]any{
"authSources": []any{},
@@ -712,7 +696,6 @@ func TestLooker(t *testing.T) {
"name": "offset",
"required": false,
"type": "integer",
"default": float64(0),
},
},
},
@@ -758,7 +741,6 @@ func TestLooker(t *testing.T) {
"description": "The filters for the query",
"name": "filters",
"required": false,
"default": map[string]any{},
"type": "object",
},
map[string]any{
@@ -774,7 +756,6 @@ func TestLooker(t *testing.T) {
"name": "pivots",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -789,7 +770,6 @@ func TestLooker(t *testing.T) {
"name": "sorts",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -797,7 +777,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(500),
},
map[string]any{
"authSources": []any{},
@@ -818,7 +797,6 @@ func TestLooker(t *testing.T) {
"description": "The description of the Look",
"name": "description",
"required": false,
"default": "",
"type": "string",
},
map[string]any{
@@ -826,7 +804,6 @@ func TestLooker(t *testing.T) {
"description": "The folder id where the Look will be created. Leave blank to use the user's personal folder",
"name": "folder",
"required": false,
"default": "",
"type": "string",
},
map[string]any{
@@ -836,7 +813,6 @@ func TestLooker(t *testing.T) {
"name": "vis_config",
"required": false,
"type": "object",
"default": map[string]any{},
},
},
},
@@ -854,7 +830,6 @@ func TestLooker(t *testing.T) {
"name": "title",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -862,7 +837,6 @@ func TestLooker(t *testing.T) {
"name": "desc",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -870,7 +844,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(100),
},
map[string]any{
"authSources": []any{},
@@ -878,7 +851,6 @@ func TestLooker(t *testing.T) {
"name": "offset",
"required": false,
"type": "integer",
"default": float64(0),
},
},
},
@@ -903,7 +875,6 @@ func TestLooker(t *testing.T) {
"name": "description",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -911,7 +882,6 @@ func TestLooker(t *testing.T) {
"name": "folder",
"required": false,
"type": "string",
"default": "",
},
},
},
@@ -950,7 +920,6 @@ func TestLooker(t *testing.T) {
"name": "filter_type",
"required": false,
"type": "string",
"default": "field_filter",
},
map[string]any{
"authSources": []any{},
@@ -986,7 +955,6 @@ func TestLooker(t *testing.T) {
"name": "allow_multiple_values",
"required": false,
"type": "boolean",
"default": true,
},
map[string]any{
"authSources": []any{},
@@ -994,7 +962,6 @@ func TestLooker(t *testing.T) {
"name": "required",
"required": false,
"type": "boolean",
"default": false,
},
},
},
@@ -1040,7 +1007,6 @@ func TestLooker(t *testing.T) {
"description": "The filters for the query",
"name": "filters",
"required": false,
"default": map[string]any{},
"type": "object",
},
map[string]any{
@@ -1056,7 +1022,6 @@ func TestLooker(t *testing.T) {
"name": "pivots",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -1071,7 +1036,6 @@ func TestLooker(t *testing.T) {
"name": "sorts",
"required": false,
"type": "array",
"default": []any{},
},
map[string]any{
"authSources": []any{},
@@ -1079,7 +1043,6 @@ func TestLooker(t *testing.T) {
"name": "limit",
"required": false,
"type": "integer",
"default": float64(500),
},
map[string]any{
"authSources": []any{},
@@ -1101,7 +1064,6 @@ func TestLooker(t *testing.T) {
"name": "title",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"additionalProperties": true,
@@ -1110,7 +1072,6 @@ func TestLooker(t *testing.T) {
"name": "vis_config",
"required": false,
"type": "object",
"default": map[string]any{},
},
map[string]any{
"authSources": []any{},
@@ -1122,7 +1083,6 @@ func TestLooker(t *testing.T) {
"name": "dashboard_filter",
"required": false,
"type": "object",
"default": map[string]any{},
},
"name": "dashboard_filters",
"required": false,
@@ -1221,7 +1181,6 @@ func TestLooker(t *testing.T) {
"name": "timeframe",
"required": false,
"type": "integer",
"default": float64(90),
},
map[string]any{
"authSources": []any{},
@@ -1229,7 +1188,6 @@ func TestLooker(t *testing.T) {
"name": "min_queries",
"required": false,
"type": "integer",
"default": float64(0),
},
},
},
@@ -1254,7 +1212,6 @@ func TestLooker(t *testing.T) {
"name": "project",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -1262,7 +1219,6 @@ func TestLooker(t *testing.T) {
"name": "model",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -1270,7 +1226,6 @@ func TestLooker(t *testing.T) {
"name": "explore",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -1278,7 +1233,6 @@ func TestLooker(t *testing.T) {
"name": "timeframe",
"required": false,
"type": "integer",
"default": float64(90),
},
map[string]any{
"authSources": []any{},
@@ -1286,7 +1240,6 @@ func TestLooker(t *testing.T) {
"name": "min_queries",
"required": false,
"type": "integer",
"default": float64(1),
},
},
},
@@ -1304,7 +1257,6 @@ func TestLooker(t *testing.T) {
"name": "devMode",
"required": false,
"type": "boolean",
"default": true,
},
},
},
@@ -1458,7 +1410,6 @@ func TestLooker(t *testing.T) {
"name": "type",
"required": false,
"type": "string",
"default": "",
},
map[string]any{
"authSources": []any{},
@@ -1466,7 +1417,6 @@ func TestLooker(t *testing.T) {
"name": "id",
"required": false,
"type": "string",
"default": "",
},
},
},

View File

@@ -165,7 +165,6 @@ func TestNeo4jToolEndpoints(t *testing.T) {
"type": "boolean",
"required": false,
"description": "If set to true, the query will be validated and information about the execution will be returned without running the query. Defaults to false.",
"default": false,
"authSources": []any{},
},
},

View File

@@ -1,178 +0,0 @@
// 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 spanneradmin
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"testing"
"time"
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
"cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
SpannerProject = os.Getenv("SPANNER_PROJECT")
)
func getSpannerAdminVars(t *testing.T) map[string]any {
if SpannerProject == "" {
t.Fatal("'SPANNER_PROJECT' not set")
}
return map[string]any{
"kind": "spanner-admin",
"defaultProject": SpannerProject,
}
}
func TestSpannerAdminCreateInstance(t *testing.T) {
sourceConfig := getSpannerAdminVars(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
defer cancel()
shortUuid := strings.ReplaceAll(uuid.New().String(), "-", "")[:10]
instanceId := "test-inst-" + shortUuid
displayName := "Test Instance " + shortUuid
instanceConfig := "regional-us-central1"
nodeCount := 1
edition := "ENTERPRISE"
// Setup Admin Client for verification and cleanup
adminClient, err := instance.NewInstanceAdminClient(ctx)
if err != nil {
t.Fatalf("unable to create Spanner instance admin client: %s", err)
}
defer adminClient.Close()
// Teardown function
defer func() {
err := adminClient.DeleteInstance(ctx, &instancepb.DeleteInstanceRequest{
Name: fmt.Sprintf("projects/%s/instances/%s", SpannerProject, instanceId),
})
if err != nil {
// If it fails, it might not have been created, log it but don't fail if it's "not found"
t.Logf("cleanup: failed to delete instance %s: %s", instanceId, err)
} else {
t.Logf("cleanup: deleted instance %s", instanceId)
}
}()
// Construct Tools Config
toolsConfig := map[string]any{
"sources": map[string]any{
"my-spanner-admin": sourceConfig,
},
"tools": map[string]any{
"create-instance-tool": map[string]any{
"kind": "spanner-create-instance",
"source": "my-spanner-admin",
"description": "Creates a Spanner instance.",
},
},
}
// Start Toolbox Server
cmd, cleanup, err := tests.StartCmd(ctx, toolsConfig)
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)
}
// Prepare Invocation Payload
payload := map[string]any{
"project": SpannerProject,
"instanceId": instanceId,
"displayName": displayName,
"config": instanceConfig,
"nodeCount": nodeCount,
"edition": edition,
"processingUnits": 0,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("failed to marshal payload: %s", err)
}
// Invoke Tool
invokeUrl := "http://127.0.0.1:5000/api/tool/create-instance-tool/invoke"
req, err := http.NewRequest(http.MethodPost, invokeUrl, bytes.NewBuffer(payloadBytes))
if err != nil {
t.Fatalf("unable to create request: %s", err)
}
req.Header.Add("Content-type", "application/json")
t.Logf("Invoking create-instance-tool for instance: %s", instanceId)
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.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
}
// Check Response
var body map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&body)
if err != nil {
t.Fatalf("error parsing response body")
}
// Verify Instance Exists via Admin Client
t.Logf("Verifying instance %s exists...", instanceId)
instanceName := fmt.Sprintf("projects/%s/instances/%s", SpannerProject, instanceId)
gotInstance, err := adminClient.GetInstance(ctx, &instancepb.GetInstanceRequest{
Name: instanceName,
})
if err != nil {
t.Fatalf("failed to get instance from admin client: %s", err)
}
if gotInstance.Name != instanceName {
t.Errorf("expected instance name %s, got %s", instanceName, gotInstance.Name)
}
if gotInstance.DisplayName != displayName {
t.Errorf("expected display name %s, got %s", displayName, gotInstance.DisplayName)
}
if gotInstance.NodeCount != int32(nodeCount) {
t.Errorf("expected node count %d, got %d", nodeCount, gotInstance.NodeCount)
}
}