mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 16:38:15 -05:00
Compare commits
25 Commits
enum-regex
...
invoke-int
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1ac73c0c | ||
|
|
cbd72c2ea1 | ||
|
|
d64b4812c3 | ||
|
|
dbc477ab0f | ||
|
|
918753d396 | ||
|
|
b43c94575d | ||
|
|
87b385b2b5 | ||
|
|
d154370cef | ||
|
|
0b3dac4132 | ||
|
|
62af39d751 | ||
|
|
67d8221a2e | ||
|
|
ab7d313f7f | ||
|
|
964a82eb08 | ||
|
|
cb692b5883 | ||
|
|
4166bf7ab8 | ||
|
|
8dfcbfd5b3 | ||
|
|
72a2366b28 | ||
|
|
9501ebbdbc | ||
|
|
a3bd2e9927 | ||
|
|
295f9dc41a | ||
|
|
58424a3f7f | ||
|
|
e5f643f929 | ||
|
|
785be3d8a4 | ||
|
|
88bac7e36f | ||
|
|
20cb43a249 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## [0.16.0](https://github.com/googleapis/genai-toolbox/compare/v0.15.0...v0.16.0) (2025-09-25)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **tool/bigquery-execute-sql:** add allowed datasets support ([#1443](https://github.com/googleapis/genai-toolbox/issues/1443))
|
||||
* **tool/bigquery-forecast:** add allowed datasets support ([#1412](https://github.com/googleapis/genai-toolbox/issues/1412))
|
||||
|
||||
### Features
|
||||
|
||||
* **cassandra:** Add Cassandra Source and Tool ([#1012](https://github.com/googleapis/genai-toolbox/issues/1012)) ([6e42053](https://github.com/googleapis/genai-toolbox/commit/6e420534ee894da4a8d226acb6cdb63d0d5d9ce5))
|
||||
* **sources/postgres:** Add application_name ([#1504](https://github.com/googleapis/genai-toolbox/issues/1504)) ([72a2366](https://github.com/googleapis/genai-toolbox/commit/72a2366b28870aa6f81c4f890f4770ec5ecffdba))
|
||||
* **tool/bigquery-execute-sql:** Add allowed datasets support ([#1443](https://github.com/googleapis/genai-toolbox/issues/1443)) ([9501ebb](https://github.com/googleapis/genai-toolbox/commit/9501ebbdbcba871b98663185c690308dda1729b5))
|
||||
* **tool/bigquery-forecast:** Add allowed datasets support ([#1412](https://github.com/googleapis/genai-toolbox/issues/1412)) ([88bac7e](https://github.com/googleapis/genai-toolbox/commit/88bac7e36f5ebb6ad18773bff30b85ef678431e7))
|
||||
* **tools/clickhouse-list-tables:** Add list-tables tool ([#1446](https://github.com/googleapis/genai-toolbox/issues/1446)) ([69a3caf](https://github.com/googleapis/genai-toolbox/commit/69a3cafabec5a40e2776d71de3587c0d16c722a2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tool/mongodb-find:** Fix find tool `limit` field ([#1570](https://github.com/googleapis/genai-toolbox/issues/1570)) ([4166bf7](https://github.com/googleapis/genai-toolbox/commit/4166bf7ab85732f64b877d5f20235057df919049))
|
||||
* **tools/mongodb:** Concat filter params only once in mongodb update tools ([#1545](https://github.com/googleapis/genai-toolbox/issues/1545)) ([295f9dc](https://github.com/googleapis/genai-toolbox/commit/295f9dc41a43f0a4bdbd99e465bf2be01249084e))
|
||||
|
||||
## [0.15.0](https://github.com/googleapis/genai-toolbox/compare/v0.14.0...v0.15.0) (2025-09-18)
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ ARG COMMIT_SHA=""
|
||||
|
||||
RUN go get ./...
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||
go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=container.${BUILD_TYPE} -X github.com/googleapis/genai-toolbox/cmd.commitSha=${COMMIT_SHA}"
|
||||
go build -ldflags "-X github.com/googleapis/genai-toolbox/cmd.buildType=${BUILD_TYPE} -X github.com/googleapis/genai-toolbox/cmd.commitSha=${COMMIT_SHA}"
|
||||
|
||||
# Final Stage
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
|
||||
80
README.md
80
README.md
@@ -33,7 +33,6 @@ documentation](https://googleapis.github.io/genai-toolbox/).
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installing the server](#installing-the-server)
|
||||
- [Running the server](#running-the-server)
|
||||
- [Homebrew Users](#homebrew-users)
|
||||
- [Integrating your application](#integrating-your-application)
|
||||
- [Configuration](#configuration)
|
||||
- [Sources](#sources)
|
||||
@@ -115,13 +114,53 @@ following instructions for your OS and CPU architecture.
|
||||
To install Toolbox as a binary:
|
||||
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.15.0
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
> <details>
|
||||
> <summary>Linux (AMD64)</summary>
|
||||
>
|
||||
> To install Toolbox as a binary on Linux (AMD64):
|
||||
> ```sh
|
||||
> # see releases page for other versions
|
||||
> export VERSION=0.16.0
|
||||
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
> chmod +x toolbox
|
||||
> ```
|
||||
>
|
||||
> </details>
|
||||
> <details>
|
||||
> <summary>macOS (Apple Silicon)</summary>
|
||||
>
|
||||
> To install Toolbox as a binary on macOS (Apple Silicon):
|
||||
> ```sh
|
||||
> # see releases page for other versions
|
||||
> export VERSION=0.16.0
|
||||
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
|
||||
> chmod +x toolbox
|
||||
> ```
|
||||
>
|
||||
> </details>
|
||||
> <details>
|
||||
> <summary>macOS (Intel)</summary>
|
||||
>
|
||||
> To install Toolbox as a binary on macOS (Intel):
|
||||
> ```sh
|
||||
> # see releases page for other versions
|
||||
> export VERSION=0.16.0
|
||||
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
|
||||
> chmod +x toolbox
|
||||
> ```
|
||||
>
|
||||
> </details>
|
||||
> <details>
|
||||
> <summary>Windows (AMD64)</summary>
|
||||
>
|
||||
> To install Toolbox as a binary on Windows (AMD64):
|
||||
> ```powershell
|
||||
> # see releases page for other versions
|
||||
> $VERSION = "0.16.0"
|
||||
> Invoke-WebRequest -Uri "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe" -OutFile "toolbox.exe"
|
||||
> ```
|
||||
>
|
||||
> </details>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -130,7 +169,7 @@ You can also install Toolbox as a container:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.15.0
|
||||
export VERSION=0.16.0
|
||||
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
|
||||
```
|
||||
|
||||
@@ -154,12 +193,23 @@ 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.15.0
|
||||
go install github.com/googleapis/genai-toolbox@v0.16.0
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Gemini CLI Extensions</summary>
|
||||
|
||||
To install Gemini CLI Extensions for MCP Toolbox, run the following command:
|
||||
|
||||
```sh
|
||||
gemini extensions install https://github.com/gemini-cli-extensions/mcp-toolbox
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Running the server
|
||||
|
||||
[Configure](#configuration) a `tools.yaml` to define your tools, and then
|
||||
@@ -233,6 +283,16 @@ toolbox --tools-file "tools.yaml"
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Gemini CLI</summary>
|
||||
|
||||
Interact with your custom tools using natural language. Check
|
||||
[gemini-cli-extensions/mcp-toolbox](https://github.com/gemini-cli-extensions/mcp-toolbox)
|
||||
for more information.
|
||||
|
||||
</details>
|
||||
|
||||
You can use `toolbox help` for a full list of flags! To stop the server, send a
|
||||
terminate signal (`ctrl+c` on most platforms).
|
||||
|
||||
|
||||
28
cmd/root.go
28
cmd/root.go
@@ -655,20 +655,6 @@ func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles m
|
||||
}
|
||||
}
|
||||
|
||||
// updateLogLevel checks if Toolbox have to update the existing log level set by users.
|
||||
// stdio doesn't support "debug" and "info" logs.
|
||||
func updateLogLevel(stdio bool, logLevel string) bool {
|
||||
if stdio {
|
||||
switch strings.ToUpper(logLevel) {
|
||||
case log.Debug, log.Info:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveWatcherInputs(toolsFile string, toolsFiles []string, toolsFolder string) (map[string]bool, map[string]bool) {
|
||||
var relevantFiles []string
|
||||
|
||||
@@ -697,10 +683,6 @@ func resolveWatcherInputs(toolsFile string, toolsFiles []string, toolsFolder str
|
||||
}
|
||||
|
||||
func run(cmd *Command) error {
|
||||
if updateLogLevel(cmd.cfg.Stdio, cmd.cfg.LogLevel.String()) {
|
||||
cmd.cfg.LogLevel = server.StringLevel(log.Warn)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
@@ -724,16 +706,22 @@ func run(cmd *Command) error {
|
||||
cancel()
|
||||
}(ctx)
|
||||
|
||||
// If stdio, set logger's out stream (usually DEBUG and INFO logs) to errStream
|
||||
loggerOut := cmd.outStream
|
||||
if cmd.cfg.Stdio {
|
||||
loggerOut = cmd.errStream
|
||||
}
|
||||
|
||||
// Handle logger separately from config
|
||||
switch strings.ToLower(cmd.cfg.LoggingFormat.String()) {
|
||||
case "json":
|
||||
logger, err := log.NewStructuredLogger(cmd.outStream, cmd.errStream, cmd.cfg.LogLevel.String())
|
||||
logger, err := log.NewStructuredLogger(loggerOut, cmd.errStream, cmd.cfg.LogLevel.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize logger: %w", err)
|
||||
}
|
||||
cmd.logger = logger
|
||||
case "standard":
|
||||
logger, err := log.NewStdLogger(cmd.outStream, cmd.errStream, cmd.cfg.LogLevel.String())
|
||||
logger, err := log.NewStdLogger(loggerOut, cmd.errStream, cmd.cfg.LogLevel.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize logger: %w", err)
|
||||
}
|
||||
|
||||
@@ -1597,51 +1597,3 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateLogLevel(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
stdio bool
|
||||
logLevel string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
desc: "no stdio",
|
||||
stdio: false,
|
||||
logLevel: "info",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "stdio with info log",
|
||||
stdio: true,
|
||||
logLevel: "info",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "stdio with debug log",
|
||||
stdio: true,
|
||||
logLevel: "debug",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "stdio with warn log",
|
||||
stdio: true,
|
||||
logLevel: "warn",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "stdio with error log",
|
||||
stdio: true,
|
||||
logLevel: "error",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := updateLogLevel(tc.stdio, tc.logLevel)
|
||||
if got != tc.want {
|
||||
t.Fatalf("incorrect indication to update log level: got %t, want %t", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.15.0
|
||||
0.16.0
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"version = \"0.15.0\" # x-release-please-version\n",
|
||||
"version = \"0.16.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",
|
||||
|
||||
@@ -81,23 +81,50 @@ following instructions for your OS and CPU architecture.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane text=true >}}
|
||||
{{% tab header="Binary" lang="en" %}}
|
||||
|
||||
To install Toolbox as a binary:
|
||||
|
||||
{{< tabpane text=true >}}
|
||||
{{% tab header="Linux (AMD64)" lang="en" %}}
|
||||
To install Toolbox as a binary on Linux (AMD64):
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.15.0
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
export VERSION=0.16.0
|
||||
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="macOS (Apple Silicon)" lang="en" %}}
|
||||
To install Toolbox as a binary on macOS (Apple Silicon):
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.16.0
|
||||
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="macOS (Intel)" lang="en" %}}
|
||||
To install Toolbox as a binary on macOS (Intel):
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.16.0
|
||||
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Windows (AMD64)" lang="en" %}}
|
||||
To install Toolbox as a binary on Windows (AMD64):
|
||||
```powershell
|
||||
# see releases page for other versions
|
||||
$VERSION = "0.16.0"
|
||||
Invoke-WebRequest -Uri "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe" -OutFile "toolbox.exe"
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
{{% /tab %}}
|
||||
{{% tab header="Container image" lang="en" %}}
|
||||
You can also install Toolbox as a container:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.15.0
|
||||
export VERSION=0.16.0
|
||||
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
|
||||
```
|
||||
|
||||
@@ -116,7 +143,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.15.0
|
||||
go install github.com/googleapis/genai-toolbox@v0.16.0
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -345,9 +345,10 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
google-adk==1.14.1
|
||||
google-adk==1.15.0
|
||||
toolbox-core==0.5.2
|
||||
pytest==8.4.2
|
||||
@@ -1,4 +1,4 @@
|
||||
llama-index==0.14.2
|
||||
llama-index-llms-google-genai==0.5.1
|
||||
llama-index==0.14.3
|
||||
llama-index-llms-google-genai==0.6.0
|
||||
toolbox-llamaindex==0.5.2
|
||||
pytest==8.4.2
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -48,19 +48,19 @@ to expose your developer assistant tools to a Looker instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -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.15.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -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.15.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -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.15.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -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.15.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -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.15.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.15.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
38
docs/en/how-to/connect_via_geminicli.md
Normal file
38
docs/en/how-to/connect_via_geminicli.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Connect via Gemini CLI Extensions
|
||||
type: docs
|
||||
weight: 2
|
||||
description: "Connect to Toolbox via Gemini CLI Extensions."
|
||||
---
|
||||
|
||||
## Gemini CLI Extensions
|
||||
|
||||
[Gemini CLI][gemini-cli] is an open-source AI agent designed to assist with development workflows by assisting with coding, debugging, data exploration, and content creation. Its mission is to provide an agentic interface for interacting with database and analytics services and popular open-source databases.
|
||||
|
||||
### How extensions work
|
||||
Gemini CLI is highly extensible, allowing for the addition of new tools and capabilities through extensions. You can load the extensions from a GitHub URL, a local directory, or a configurable registry. They provide new tools, slash commands, and prompts to assist with your workflow.
|
||||
|
||||
Use the Gemini CLI Extensions to load prebuilt or custom tools to interact with your databases.
|
||||
|
||||
[gemini-cli]: https://google-gemini.github.io/gemini-cli/
|
||||
|
||||
Below are a list of Gemini CLI Extensions powered by MCP Toolbox:
|
||||
|
||||
* [alloydb](https://github.com/gemini-cli-extensions/alloydb)
|
||||
* [alloydb-observability](https://github.com/gemini-cli-extensions/alloydb-observability)
|
||||
* [bigquery-conversational-analytics](https://github.com/gemini-cli-extensions/bigquery-conversational-analytics)
|
||||
* [bigquery-data-analytics](https://github.com/gemini-cli-extensions/bigquery-data-analytics)
|
||||
* [cloud-sql-mysql](https://github.com/gemini-cli-extensions/cloud-sql-mysql)
|
||||
* [cloud-sql-mysql-observability](https://github.com/gemini-cli-extensions/cloud-sql-mysql-observability)
|
||||
* [cloud-sql-postgresql](https://github.com/gemini-cli-extensions/cloud-sql-postgresql)
|
||||
* [cloud-sql-postgresql-observability](https://github.com/gemini-cli-extensions/cloud-sql-postgresql-observability)
|
||||
* [cloud-sql-sqlserver](https://github.com/gemini-cli-extensions/cloud-sql-sqlserver)
|
||||
* [cloud-sql-sqlserver-observability](https://github.com/gemini-cli-extensions/cloud-sql-sqlserver-observability)
|
||||
* [dataplex](https://github.com/gemini-cli-extensions/dataplex)
|
||||
* [firestore-native](https://github.com/gemini-cli-extensions/firestore-native)
|
||||
* [looker](https://github.com/gemini-cli-extensions/looker)
|
||||
* [mcp-toolbox](https://github.com/gemini-cli-extensions/mcp-toolbox)
|
||||
* [mysql](https://github.com/gemini-cli-extensions/mysql)
|
||||
* [postgres](https://github.com/gemini-cli-extensions/postgres)
|
||||
* [spanner](https://github.com/gemini-cli-extensions/spanner)
|
||||
* [sql-server](https://github.com/gemini-cli-extensions/sql-server)
|
||||
@@ -15,9 +15,23 @@ It's compatible with the following sources:
|
||||
|
||||
- [bigquery](../../sources/bigquery.md)
|
||||
|
||||
`bigquery-execute-sql` takes a required `sql` input parameter and runs the SQL
|
||||
statement against the configured `source`. It also supports an optional `dry_run`
|
||||
parameter to validate a query without executing it.
|
||||
`bigquery-execute-sql` accepts the following parameters:
|
||||
- **`sql`** (required): The GoogleSQL statement to execute.
|
||||
- **`dry_run`** (optional): If set to `true`, the query is validated but not run,
|
||||
returning information about the execution instead. Defaults to `false`.
|
||||
|
||||
The tool's behavior is influenced by the `allowedDatasets` restriction on the
|
||||
`bigquery` source:
|
||||
- **Without `allowedDatasets` restriction:** The tool can execute any valid GoogleSQL
|
||||
query.
|
||||
- **With `allowedDatasets` restriction:** Before execution, the tool performs a dry run
|
||||
to analyze the query.
|
||||
It will reject the query if it attempts to access any table outside the
|
||||
allowed `datasets` list. To enforce this restriction, the following operations
|
||||
are also disallowed:
|
||||
- **Dataset-level operations** (e.g., `CREATE SCHEMA`, `ALTER SCHEMA`).
|
||||
- **Unanalyzable operations** where the accessed tables cannot be determined
|
||||
statically (e.g., `EXECUTE IMMEDIATE`, `CREATE PROCEDURE`, `CALL`).
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -33,6 +33,13 @@ query based on the provided parameters:
|
||||
- **horizon** (integer, optional): The number of future time steps you want to
|
||||
predict. It defaults to 10 if not specified.
|
||||
|
||||
The tool's behavior regarding these parameters is influenced by the `allowedDatasets` restriction on the `bigquery` source:
|
||||
- **Without `allowedDatasets` restriction:** The tool can use any table or query for the `history_data` parameter.
|
||||
- **With `allowedDatasets` restriction:** The tool verifies that the `history_data` parameter only accesses tables
|
||||
within the allowed datasets. If `history_data` is a table ID, the tool checks if the table's dataset is in the
|
||||
allowed list. If `history_data` is a query, the tool performs a dry run to analyze the query and rejects it
|
||||
if it accesses any table outside the allowed list.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -297,7 +297,8 @@
|
||||
"setup_queries = [\n",
|
||||
" # Install required extension to use the AlloyDB AI natural language support API\n",
|
||||
" \"\"\"CREATE EXTENSION IF NOT EXISTS alloydb_ai_nl cascade;\"\"\",\n",
|
||||
"\n",
|
||||
" \"\"\"CREATE EXTENSION IF NOT EXISTS google_ml_integration; \"\"\",\n",
|
||||
" \n",
|
||||
" # Create schema\n",
|
||||
" \"\"\"CREATE SCHEMA IF NOT EXISTS nla_demo;\"\"\",\n",
|
||||
"\n",
|
||||
@@ -689,15 +690,18 @@
|
||||
"source": [
|
||||
"async def run_queries(pool):\n",
|
||||
" async with pool.connect() as db_conn:\n",
|
||||
"\n",
|
||||
" # Verify the generated context for the tables\n",
|
||||
" for query in verify_context_queries:\n",
|
||||
" response = await db_conn.execute(sqlalchemy.text(query))\n",
|
||||
" print(\"Verify the context:\", response.mappings().all())\n",
|
||||
"\n",
|
||||
" \n",
|
||||
" # Update context that needs revision\n",
|
||||
" for query in update_context_queries:\n",
|
||||
" await db_conn.execute(sqlalchemy.text(query))\n",
|
||||
"\n",
|
||||
" \n",
|
||||
" # The resulting context entries in the alloydb_ai_nl.generated_schema_context_view \n",
|
||||
" # view are applied to the corresponding schema objects, and the comments are overwritten.\n",
|
||||
" for query in apply_generated_context_queries:\n",
|
||||
@@ -767,7 +771,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"version = \"0.15.0\" # x-release-please-version\n",
|
||||
"version = \"0.16.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",
|
||||
|
||||
@@ -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.15.0"
|
||||
export VERSION="0.16.0"
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"version = \"0.15.0\" # x-release-please-version\n",
|
||||
"version = \"0.16.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",
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -64,6 +64,7 @@ In this section, we will download Toolbox and run the Toolbox server.
|
||||
```bash
|
||||
export LOOKER_BASE_URL=https://looker.example.com
|
||||
export LOOKER_VERIFY_SSL=true
|
||||
export LOOKER_USE_CLIENT_OAUTH=true
|
||||
```
|
||||
|
||||
In some instances you may need to append `:19999` to
|
||||
|
||||
@@ -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.15.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.16.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
20
go.mod
20
go.mod
@@ -6,10 +6,10 @@ toolchain go1.25.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/alloydbconn v1.15.5
|
||||
cloud.google.com/go/bigquery v1.70.0
|
||||
cloud.google.com/go/bigquery v1.71.0
|
||||
cloud.google.com/go/bigtable v1.40.0
|
||||
cloud.google.com/go/cloudsqlconn v1.18.1
|
||||
cloud.google.com/go/dataplex v1.27.0
|
||||
cloud.google.com/go/dataplex v1.27.1
|
||||
cloud.google.com/go/firestore v1.18.0
|
||||
cloud.google.com/go/spanner v1.85.1
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.3
|
||||
@@ -51,8 +51,8 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
google.golang.org/api v0.249.0
|
||||
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1
|
||||
google.golang.org/api v0.250.0
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9
|
||||
modernc.org/sqlite v1.39.0
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ require (
|
||||
cloud.google.com/go/alloydb v1.18.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.4 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
@@ -171,13 +171,13 @@ require (
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
|
||||
48
go.sum
48
go.sum
@@ -137,8 +137,8 @@ cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/Zur
|
||||
cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac=
|
||||
cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q=
|
||||
cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
|
||||
cloud.google.com/go/bigquery v1.70.0 h1:V1OIhhOSionCOXWMmypXOvZu/ogkzosa7s1ArWJO/Yg=
|
||||
cloud.google.com/go/bigquery v1.70.0/go.mod h1:6lEAkgTJN+H2JcaX1eKiuEHTKyqBaJq5U3SpLGbSvwI=
|
||||
cloud.google.com/go/bigquery v1.71.0 h1:NvSZvXU1Hyb+YiRVKQPuQXGeZaw/0NP6M/WOrBqSx3g=
|
||||
cloud.google.com/go/bigquery v1.71.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ=
|
||||
cloud.google.com/go/bigtable v1.40.0 h1:iNeqGqkJvFdjg07Ku3F7KKfq5QZvBySisYHVsLB1RwE=
|
||||
cloud.google.com/go/bigtable v1.40.0/go.mod h1:LtPzCcrAFaGRZ82Hs8xMueUeYW9Jw12AmNdUTMfDnh4=
|
||||
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
|
||||
@@ -194,8 +194,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
|
||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||
cloud.google.com/go/compute/metadata v0.8.4 h1:oXMa1VMQBVCyewMIOm3WQsnVd9FbKBtm8reqWRaXnHQ=
|
||||
cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
|
||||
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
|
||||
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
|
||||
@@ -216,8 +216,8 @@ cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOX
|
||||
cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M=
|
||||
cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0=
|
||||
cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8=
|
||||
cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE=
|
||||
cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14=
|
||||
cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k=
|
||||
cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg=
|
||||
cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
|
||||
cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
|
||||
cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE=
|
||||
@@ -236,8 +236,8 @@ cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX
|
||||
cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A=
|
||||
cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ=
|
||||
cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs=
|
||||
cloud.google.com/go/dataplex v1.27.0 h1:k6gf5DnX7YHD/hilShxVP9ExmGrEWZFjfBZ7rHt4JlM=
|
||||
cloud.google.com/go/dataplex v1.27.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA=
|
||||
cloud.google.com/go/dataplex v1.27.1 h1:renSEYTQZMQ3ag7lM0BDmSj4FWqaTGW60YQ/lvAE5iA=
|
||||
cloud.google.com/go/dataplex v1.27.1/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA=
|
||||
cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s=
|
||||
cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI=
|
||||
cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4=
|
||||
@@ -1050,11 +1050,11 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
@@ -1687,8 +1687,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -1834,8 +1834,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
|
||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w=
|
||||
google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ=
|
||||
google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM=
|
||||
google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1976,12 +1976,12 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=
|
||||
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -2023,8 +2023,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@@ -2043,8 +2043,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -75,11 +75,17 @@ func (t MockTool) RequiresClientAuthorization() bool {
|
||||
func (t MockTool) McpManifest() tools.McpManifest {
|
||||
properties := make(map[string]tools.ParameterMcpManifest)
|
||||
required := make([]string, 0)
|
||||
authParams := make(map[string][]string)
|
||||
|
||||
for _, p := range t.Params {
|
||||
name := p.GetName()
|
||||
properties[name] = p.McpManifest()
|
||||
paramManifest, authParamList := p.McpManifest()
|
||||
properties[name] = paramManifest
|
||||
required = append(required, name)
|
||||
|
||||
if len(authParamList) > 0 {
|
||||
authParams[name] = authParamList
|
||||
}
|
||||
}
|
||||
|
||||
toolsSchema := tools.McpToolsSchema{
|
||||
@@ -88,11 +94,19 @@ func (t MockTool) McpManifest() tools.McpManifest {
|
||||
Required: required,
|
||||
}
|
||||
|
||||
return tools.McpManifest{
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
InputSchema: toolsSchema,
|
||||
}
|
||||
|
||||
if len(authParams) > 0 {
|
||||
mcpManifest.Metadata = map[string]any{
|
||||
"toolbox/authParams": authParams,
|
||||
}
|
||||
}
|
||||
|
||||
return mcpManifest
|
||||
}
|
||||
|
||||
var tool1 = MockTool{
|
||||
|
||||
@@ -117,11 +117,15 @@ func getOpts(ipType, userAgent string, useIAM bool) ([]alloydbconn.Option, error
|
||||
}
|
||||
|
||||
func getConnectionConfig(ctx context.Context, user, pass, dbname string) (string, bool, error) {
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
userAgent = "genai-toolbox"
|
||||
}
|
||||
useIAM := true
|
||||
|
||||
// If username and password both provided, use password authentication
|
||||
if user != "" && pass != "" {
|
||||
dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, pass, dbname)
|
||||
dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable application_name=%s", user, pass, dbname, userAgent)
|
||||
useIAM = false
|
||||
return dsn, useIAM, nil
|
||||
}
|
||||
@@ -141,7 +145,7 @@ func getConnectionConfig(ctx context.Context, user, pass, dbname string) (string
|
||||
}
|
||||
|
||||
// Construct IAM connection string with username
|
||||
dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable", user, dbname)
|
||||
dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable application_name=%s", user, dbname, userAgent)
|
||||
return dsn, useIAM, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestParseFromYamlCassandra(t *testing.T) {
|
||||
sources:
|
||||
my-cassandra-instance:
|
||||
kind: cassandra
|
||||
hosts:
|
||||
hosts:
|
||||
- "my-host1"
|
||||
- "my-host2"
|
||||
`,
|
||||
@@ -62,7 +62,7 @@ func TestParseFromYamlCassandra(t *testing.T) {
|
||||
sources:
|
||||
my-cassandra-instance:
|
||||
kind: cassandra
|
||||
hosts:
|
||||
hosts:
|
||||
- "my-host1"
|
||||
- "my-host2"
|
||||
username: "user"
|
||||
@@ -121,11 +121,11 @@ func TestFailParseFromYaml(t *testing.T) {
|
||||
sources:
|
||||
my-cassandra-instance:
|
||||
kind: cassandra
|
||||
host:
|
||||
hosts:
|
||||
- "my-host"
|
||||
foo: bar
|
||||
`,
|
||||
err: "unable to parse source \"my-cassandra-instance\" as \"cassandra\": [1:1] unknown field \"foo\"\n> 1 | foo: bar\n ^\n 2 | host:\n 3 | - my-host\n 4 | kind: cassandra",
|
||||
err: "unable to parse source \"my-cassandra-instance\" as \"cassandra\": [1:1] unknown field \"foo\"\n> 1 | foo: bar\n ^\n 2 | hosts:\n 3 | - my-host\n 4 | kind: cassandra",
|
||||
},
|
||||
{
|
||||
desc: "missing required field",
|
||||
|
||||
@@ -98,11 +98,15 @@ func (s *Source) PostgresPool() *pgxpool.Pool {
|
||||
}
|
||||
|
||||
func getConnectionConfig(ctx context.Context, user, pass, dbname string) (string, bool, error) {
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
userAgent = "genai-toolbox"
|
||||
}
|
||||
useIAM := true
|
||||
|
||||
// If username and password both provided, use password authentication
|
||||
if user != "" && pass != "" {
|
||||
dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, pass, dbname)
|
||||
dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable application_name=%s", user, pass, dbname, userAgent)
|
||||
useIAM = false
|
||||
return dsn, useIAM, nil
|
||||
}
|
||||
@@ -122,7 +126,7 @@ func getConnectionConfig(ctx context.Context, user, pass, dbname string) (string
|
||||
}
|
||||
|
||||
// Construct IAM connection string with username
|
||||
dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable", user, dbname)
|
||||
dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable application_name=%s", user, dbname, userAgent)
|
||||
return dsn, useIAM, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
BaseURL string `yaml:"base_url" validate:"required"`
|
||||
ClientId string `yaml:"client_id" validate:"required"`
|
||||
ClientSecret string `yaml:"client_secret" validate:"required"`
|
||||
ClientId string `yaml:"client_id"`
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
SslVerification bool `yaml:"verify_ssl"`
|
||||
UseClientOAuth bool `yaml:"use_client_oauth"`
|
||||
Timeout string `yaml:"timeout"`
|
||||
|
||||
@@ -100,10 +100,8 @@ func TestFailParseFromYamlLooker(t *testing.T) {
|
||||
sources:
|
||||
my-looker-instance:
|
||||
kind: looker
|
||||
base_url: http://example.looker.com/
|
||||
client_id: jasdl;k;tjl
|
||||
`,
|
||||
err: "unable to parse source \"my-looker-instance\" as \"looker\": Key: 'Config.ClientSecret' Error:Field validation for 'ClientSecret' failed on the 'required' tag",
|
||||
err: "unable to parse source \"my-looker-instance\" as \"looker\": Key: 'Config.BaseURL' Error:Field validation for 'BaseURL' failed on the 'required' tag",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@@ -91,14 +93,46 @@ func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) PostgresPool() *pgxpool.Pool {
|
||||
return s.Pool
|
||||
func (s *Source) RunSQL(ctx context.Context, statement string, params any) (any, error) {
|
||||
sliceParams := params.(tools.ParamValues).AsSlice()
|
||||
results, err := s.Pool.Query(ctx, statement, sliceParams...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
|
||||
fields := results.FieldDescriptions()
|
||||
|
||||
var out []any
|
||||
for results.Next() {
|
||||
v, err := results.Values()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
vMap := make(map[string]any)
|
||||
for i, f := range fields {
|
||||
vMap[f.Name] = v[i]
|
||||
}
|
||||
out = append(out, vMap)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string, queryParams map[string]string) (*pgxpool.Pool, error) {
|
||||
//nolint:all // Reassigned ctx
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
userAgent = "genai-toolbox"
|
||||
}
|
||||
if queryParams == nil {
|
||||
// Initialize the map before using it
|
||||
queryParams = make(map[string]string)
|
||||
}
|
||||
if _, ok := queryParams["application_name"]; !ok {
|
||||
queryParams["application_name"] = userAgent
|
||||
}
|
||||
|
||||
// urlExample := "postgres:dd//username:password@localhost:5432/database_name"
|
||||
url := &url.URL{
|
||||
|
||||
@@ -80,19 +80,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "cluster", "password"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a new AlloyDB cluster. This is a long-running operation, but the API call returns quickly. This will return operation id to be used by get operations tool. Take all parameters from user in one go."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -81,19 +81,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "cluster", "instance"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a new AlloyDB instance (PRIMARY or READ_POOL) within a cluster. This is a long-running operation. This will return operation id to be used by get operations tool. Take all parameters from user in one go."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -81,19 +81,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "cluster", "user", "userType"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a new AlloyDB user within a cluster. Takes the new user's name and a secure password. Optionally, a list of database roles can be assigned. Always ask the user for the type of user to create. ALLOYDB_IAM_USER is recommended."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -77,19 +77,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "cluster"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Retrieves details about a specific AlloyDB cluster."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -78,19 +78,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "cluster", "instance"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Retrieves details about a specific AlloyDB instance."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -78,19 +78,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "cluster", "user"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Retrieves details about a specific AlloyDB user."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -76,19 +76,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Lists all AlloyDB clusters in a given project and location."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -77,19 +77,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Lists all AlloyDB instances in a given project, location and cluster."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -77,19 +77,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "cluster"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Lists all AlloyDB users in a given project, location and cluster."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -129,19 +129,12 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "location", "operation"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "This will poll on operations API until the operation is done. For checking operation status we need projectId, locationID and operationId. Once instance is created give follow up steps on how to use the variables to bring data plane MCP server up in local and remote setup."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
var delay time.Duration
|
||||
if cfg.Delay == "" {
|
||||
|
||||
@@ -119,11 +119,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
cfg.NLConfigParameters = append([]tools.Parameter{newQuestionParam}, cfg.NLConfigParameters...)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: cfg.NLConfigParameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, cfg.NLConfigParameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -118,11 +118,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
pruningMethodParameter,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
431
internal/tools/bigquery/bigquerycommon/table_name_parser.go
Normal file
431
internal/tools/bigquery/bigquerycommon/table_name_parser.go
Normal file
@@ -0,0 +1,431 @@
|
||||
// Copyright 2025 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 bigquerycommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// parserState defines the state of the SQL parser's state machine.
|
||||
type parserState int
|
||||
|
||||
const (
|
||||
stateNormal parserState = iota
|
||||
// String states
|
||||
stateInSingleQuoteString
|
||||
stateInDoubleQuoteString
|
||||
stateInTripleSingleQuoteString
|
||||
stateInTripleDoubleQuoteString
|
||||
stateInRawSingleQuoteString
|
||||
stateInRawDoubleQuoteString
|
||||
stateInRawTripleSingleQuoteString
|
||||
stateInRawTripleDoubleQuoteString
|
||||
// Comment states
|
||||
stateInSingleLineCommentDash
|
||||
stateInSingleLineCommentHash
|
||||
stateInMultiLineComment
|
||||
)
|
||||
|
||||
// SQL statement verbs
|
||||
const (
|
||||
verbCreate = "create"
|
||||
verbAlter = "alter"
|
||||
verbDrop = "drop"
|
||||
verbSelect = "select"
|
||||
verbInsert = "insert"
|
||||
verbUpdate = "update"
|
||||
verbDelete = "delete"
|
||||
verbMerge = "merge"
|
||||
)
|
||||
|
||||
var tableFollowsKeywords = map[string]bool{
|
||||
"from": true,
|
||||
"join": true,
|
||||
"update": true,
|
||||
"into": true, // INSERT INTO, MERGE INTO
|
||||
"table": true, // CREATE TABLE, ALTER TABLE
|
||||
"using": true, // MERGE ... USING
|
||||
"insert": true, // INSERT my_table
|
||||
"merge": true, // MERGE my_table
|
||||
}
|
||||
|
||||
var tableContextExitKeywords = map[string]bool{
|
||||
"where": true,
|
||||
"group": true, // GROUP BY
|
||||
"having": true,
|
||||
"order": true, // ORDER BY
|
||||
"limit": true,
|
||||
"window": true,
|
||||
"on": true, // JOIN ... ON
|
||||
"set": true, // UPDATE ... SET
|
||||
"when": true, // MERGE ... WHEN
|
||||
}
|
||||
|
||||
// TableParser is the main entry point for parsing a SQL string to find all referenced table IDs.
|
||||
// It handles multi-statement SQL, comments, and recursive parsing of EXECUTE IMMEDIATE statements.
|
||||
func TableParser(sql, defaultProjectID string) ([]string, error) {
|
||||
tableIDSet := make(map[string]struct{})
|
||||
visitedSQLs := make(map[string]struct{})
|
||||
if _, err := parseSQL(sql, defaultProjectID, tableIDSet, visitedSQLs, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tableIDs := make([]string, 0, len(tableIDSet))
|
||||
for id := range tableIDSet {
|
||||
tableIDs = append(tableIDs, id)
|
||||
}
|
||||
return tableIDs, nil
|
||||
}
|
||||
|
||||
// parseSQL is the core recursive function that processes SQL strings.
|
||||
// It uses a state machine to find table names and recursively parse EXECUTE IMMEDIATE.
|
||||
func parseSQL(sql, defaultProjectID string, tableIDSet map[string]struct{}, visitedSQLs map[string]struct{}, inSubquery bool) (int, error) {
|
||||
// Prevent infinite recursion.
|
||||
if _, ok := visitedSQLs[sql]; ok {
|
||||
return len(sql), nil
|
||||
}
|
||||
visitedSQLs[sql] = struct{}{}
|
||||
|
||||
state := stateNormal
|
||||
expectingTable := false
|
||||
var lastTableKeyword, lastToken, statementVerb string
|
||||
runes := []rune(sql)
|
||||
|
||||
for i := 0; i < len(runes); {
|
||||
char := runes[i]
|
||||
remaining := sql[i:]
|
||||
|
||||
switch state {
|
||||
case stateNormal:
|
||||
if strings.HasPrefix(remaining, "--") {
|
||||
state = stateInSingleLineCommentDash
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, "#") {
|
||||
state = stateInSingleLineCommentHash
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, "/*") {
|
||||
state = stateInMultiLineComment
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if char == '(' {
|
||||
if expectingTable {
|
||||
// The subquery starts after '('.
|
||||
consumed, err := parseSQL(remaining[1:], defaultProjectID, tableIDSet, visitedSQLs, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Advance i by the length of the subquery + the opening parenthesis.
|
||||
// The recursive call returns what it consumed, including the closing parenthesis.
|
||||
i += consumed + 1
|
||||
// For most keywords, we expect only one table. `from` can have multiple "tables" (subqueries).
|
||||
if lastTableKeyword != "from" {
|
||||
expectingTable = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if char == ')' {
|
||||
if inSubquery {
|
||||
return i + 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
if char == ';' {
|
||||
statementVerb = ""
|
||||
lastToken = ""
|
||||
i++
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
// Raw strings must be checked before regular strings.
|
||||
if strings.HasPrefix(remaining, "r'''") || strings.HasPrefix(remaining, "R'''") {
|
||||
state = stateInRawTripleSingleQuoteString
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, `r"""`) || strings.HasPrefix(remaining, `R"""`) {
|
||||
state = stateInRawTripleDoubleQuoteString
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, "r'") || strings.HasPrefix(remaining, "R'") {
|
||||
state = stateInRawSingleQuoteString
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, `r"`) || strings.HasPrefix(remaining, `R"`) {
|
||||
state = stateInRawDoubleQuoteString
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, "'''") {
|
||||
state = stateInTripleSingleQuoteString
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(remaining, `"""`) {
|
||||
state = stateInTripleDoubleQuoteString
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if char == '\'' {
|
||||
state = stateInSingleQuoteString
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if char == '"' {
|
||||
state = stateInDoubleQuoteString
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsLetter(char) || char == '`' {
|
||||
parts, consumed, err := parseIdentifierSequence(remaining)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if consumed == 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
keyword := strings.ToLower(parts[0])
|
||||
switch keyword {
|
||||
case "call":
|
||||
return 0, fmt.Errorf("CALL is not allowed when dataset restrictions are in place, as the called procedure's contents cannot be safely analyzed")
|
||||
case "immediate":
|
||||
if lastToken == "execute" {
|
||||
return 0, fmt.Errorf("EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place, as its contents cannot be safely analyzed")
|
||||
}
|
||||
case "procedure", "function":
|
||||
if lastToken == "create" || lastToken == "create or replace" {
|
||||
return 0, fmt.Errorf("unanalyzable statements like '%s %s' are not allowed", strings.ToUpper(lastToken), strings.ToUpper(keyword))
|
||||
}
|
||||
case verbCreate, verbAlter, verbDrop, verbSelect, verbInsert, verbUpdate, verbDelete, verbMerge:
|
||||
if statementVerb == "" {
|
||||
statementVerb = keyword
|
||||
}
|
||||
}
|
||||
|
||||
if statementVerb == verbCreate || statementVerb == verbAlter || statementVerb == verbDrop {
|
||||
if keyword == "schema" || keyword == "dataset" {
|
||||
return 0, fmt.Errorf("dataset-level operations like '%s %s' are not allowed when dataset restrictions are in place", strings.ToUpper(statementVerb), strings.ToUpper(keyword))
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := tableFollowsKeywords[keyword]; ok {
|
||||
expectingTable = true
|
||||
lastTableKeyword = keyword
|
||||
} else if _, ok := tableContextExitKeywords[keyword]; ok {
|
||||
expectingTable = false
|
||||
lastTableKeyword = ""
|
||||
}
|
||||
if lastToken == "create" && keyword == "or" {
|
||||
lastToken = "create or"
|
||||
} else if lastToken == "create or" && keyword == "replace" {
|
||||
lastToken = "create or replace"
|
||||
} else {
|
||||
lastToken = keyword
|
||||
}
|
||||
} else if len(parts) >= 2 {
|
||||
// This is a multi-part identifier. If we were expecting a table, this is it.
|
||||
if expectingTable {
|
||||
tableID, err := formatTableID(parts, defaultProjectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if tableID != "" {
|
||||
tableIDSet[tableID] = struct{}{}
|
||||
}
|
||||
// For most keywords, we expect only one table.
|
||||
if lastTableKeyword != "from" {
|
||||
expectingTable = false
|
||||
}
|
||||
}
|
||||
lastToken = ""
|
||||
}
|
||||
|
||||
i += consumed
|
||||
continue
|
||||
}
|
||||
i++
|
||||
|
||||
case stateInSingleQuoteString:
|
||||
if char == '\\' {
|
||||
i += 2 // Skip backslash and the escaped character.
|
||||
continue
|
||||
}
|
||||
if char == '\'' {
|
||||
state = stateNormal
|
||||
}
|
||||
i++
|
||||
case stateInDoubleQuoteString:
|
||||
if char == '\\' {
|
||||
i += 2 // Skip backslash and the escaped character.
|
||||
continue
|
||||
}
|
||||
if char == '"' {
|
||||
state = stateNormal
|
||||
}
|
||||
i++
|
||||
case stateInTripleSingleQuoteString:
|
||||
if strings.HasPrefix(remaining, "'''") {
|
||||
state = stateNormal
|
||||
i += 3
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
case stateInTripleDoubleQuoteString:
|
||||
if strings.HasPrefix(remaining, `"""`) {
|
||||
state = stateNormal
|
||||
i += 3
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
case stateInSingleLineCommentDash, stateInSingleLineCommentHash:
|
||||
if char == '\n' {
|
||||
state = stateNormal
|
||||
}
|
||||
i++
|
||||
case stateInMultiLineComment:
|
||||
if strings.HasPrefix(remaining, "*/") {
|
||||
state = stateNormal
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
case stateInRawSingleQuoteString:
|
||||
if char == '\'' {
|
||||
state = stateNormal
|
||||
}
|
||||
i++
|
||||
case stateInRawDoubleQuoteString:
|
||||
if char == '"' {
|
||||
state = stateNormal
|
||||
}
|
||||
i++
|
||||
case stateInRawTripleSingleQuoteString:
|
||||
if strings.HasPrefix(remaining, "'''") {
|
||||
state = stateNormal
|
||||
i += 3
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
case stateInRawTripleDoubleQuoteString:
|
||||
if strings.HasPrefix(remaining, `"""`) {
|
||||
state = stateNormal
|
||||
i += 3
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inSubquery {
|
||||
return 0, fmt.Errorf("unclosed subquery parenthesis")
|
||||
}
|
||||
return len(sql), nil
|
||||
}
|
||||
|
||||
// parseIdentifierSequence parses a sequence of dot-separated identifiers.
|
||||
// It returns the parts of the identifier, the number of characters consumed, and an error.
|
||||
func parseIdentifierSequence(s string) ([]string, int, error) {
|
||||
var parts []string
|
||||
var totalConsumed int
|
||||
|
||||
for {
|
||||
remaining := s[totalConsumed:]
|
||||
trimmed := strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||
totalConsumed += len(remaining) - len(trimmed)
|
||||
current := s[totalConsumed:]
|
||||
|
||||
if len(current) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
var part string
|
||||
var consumed int
|
||||
|
||||
if current[0] == '`' {
|
||||
end := strings.Index(current[1:], "`")
|
||||
if end == -1 {
|
||||
return nil, 0, fmt.Errorf("unclosed backtick identifier")
|
||||
}
|
||||
part = current[1 : end+1]
|
||||
consumed = end + 2
|
||||
} else if len(current) > 0 && unicode.IsLetter(rune(current[0])) {
|
||||
end := strings.IndexFunc(current, func(r rune) bool {
|
||||
return !unicode.IsLetter(r) && !unicode.IsNumber(r) && r != '_' && r != '-'
|
||||
})
|
||||
if end == -1 {
|
||||
part = current
|
||||
consumed = len(current)
|
||||
} else {
|
||||
part = current[:end]
|
||||
consumed = end
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
if current[0] == '`' && strings.Contains(part, ".") {
|
||||
// This handles cases like `project.dataset.table` but not `project.dataset`.table.
|
||||
// If the character after the quoted identifier is not a dot, we treat it as a full name.
|
||||
if len(current) <= consumed || current[consumed] != '.' {
|
||||
parts = append(parts, strings.Split(part, ".")...)
|
||||
totalConsumed += consumed
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
parts = append(parts, strings.Split(part, ".")...)
|
||||
totalConsumed += consumed
|
||||
|
||||
if len(s) <= totalConsumed || s[totalConsumed] != '.' {
|
||||
break
|
||||
}
|
||||
totalConsumed++
|
||||
}
|
||||
return parts, totalConsumed, nil
|
||||
}
|
||||
|
||||
func formatTableID(parts []string, defaultProjectID string) (string, error) {
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
// Not a table identifier (could be a CTE, column, etc.).
|
||||
// Return the consumed length so the main loop can skip this identifier.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var tableID string
|
||||
if len(parts) == 3 { // project.dataset.table
|
||||
tableID = strings.Join(parts, ".")
|
||||
} else { // dataset.table
|
||||
if defaultProjectID == "" {
|
||||
return "", fmt.Errorf("query contains table '%s' without project ID, and no default project ID is provided", strings.Join(parts, "."))
|
||||
}
|
||||
tableID = fmt.Sprintf("%s.%s", defaultProjectID, strings.Join(parts, "."))
|
||||
}
|
||||
|
||||
return tableID, nil
|
||||
}
|
||||
496
internal/tools/bigquery/bigquerycommon/table_name_parser_test.go
Normal file
496
internal/tools/bigquery/bigquerycommon/table_name_parser_test.go
Normal file
@@ -0,0 +1,496 @@
|
||||
// Copyright 2025 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 bigquerycommon_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerycommon"
|
||||
)
|
||||
|
||||
func TestTableParser(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sql string
|
||||
defaultProjectID string
|
||||
want []string
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "single fully qualified table",
|
||||
sql: "SELECT * FROM `my-project.my_dataset.my_table`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple statements with same table",
|
||||
sql: "select * from proj1.data1.tbl1 limit 1; select A.b from proj1.data1.tbl1 as A limit 1;",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple fully qualified tables",
|
||||
sql: "SELECT * FROM `proj1.data1`.`tbl1` JOIN proj2.`data2.tbl2` ON id",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1", "proj2.data2.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate tables",
|
||||
sql: "SELECT * FROM `proj1.data1.tbl1` JOIN proj1.data1.tbl1 ON id",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "partial table with default project",
|
||||
sql: "SELECT * FROM `my_dataset`.my_table",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"default-proj.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "partial table without default project",
|
||||
sql: "SELECT * FROM `my_dataset.my_table`",
|
||||
defaultProjectID: "",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "mixed fully qualified and partial tables",
|
||||
sql: "SELECT t1.*, t2.* FROM `proj1.data1.tbl1` AS t1 JOIN `data2.tbl2` AS t2 ON t1.id = t2.id",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1", "default-proj.data2.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no tables",
|
||||
sql: "SELECT 1+1",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ignore single part identifiers (like CTEs)",
|
||||
sql: "WITH my_cte AS (SELECT 1) SELECT * FROM `my_cte`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex CTE",
|
||||
sql: "WITH cte1 AS (SELECT * FROM `real.table.one`), cte2 AS (SELECT * FROM cte1) SELECT * FROM cte2 JOIN `real.table.two` ON true",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.one", "real.table.two"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nested subquery should be parsed",
|
||||
sql: "SELECT * FROM (SELECT a FROM (SELECT A.b FROM `real.table.nested` AS A))",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.nested"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "from clause with unnest",
|
||||
sql: "SELECT event.name FROM `my-project.my_dataset.my_table` AS A, UNNEST(A.events) AS event",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ignore more than 3 parts",
|
||||
sql: "SELECT * FROM `proj.data.tbl.col`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex query",
|
||||
sql: "SELECT name FROM (SELECT name FROM `proj1.data1.tbl1`) UNION ALL SELECT name FROM `data2.tbl2`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1", "default-proj.data2.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty sql",
|
||||
sql: "",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with comments",
|
||||
sql: "SELECT * FROM `proj1.data1.tbl1`; -- comment `fake.table.one` \n SELECT * FROM `proj2.data2.tbl2`; # comment `fake.table.two`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1", "proj2.data2.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multi-statement with semicolon",
|
||||
sql: "SELECT * FROM `proj1.data1.tbl1`; SELECT * FROM `proj2.data2.tbl2`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1", "proj2.data2.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "simple execute immediate",
|
||||
sql: "EXECUTE IMMEDIATE 'SELECT * FROM `exec.proj.tbl`'",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "execute immediate with multiple spaces",
|
||||
sql: "EXECUTE IMMEDIATE 'SELECT 1'",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "execute immediate with newline",
|
||||
sql: "EXECUTE\nIMMEDIATE 'SELECT 1'",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "execute immediate with comment",
|
||||
sql: "EXECUTE -- some comment\n IMMEDIATE 'SELECT * FROM `exec.proj.tbl`'",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "nested execute immediate",
|
||||
sql: "EXECUTE IMMEDIATE \"EXECUTE IMMEDIATE '''SELECT * FROM `nested.exec.tbl`'''\"",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "begin execute immediate",
|
||||
sql: "BEGIN EXECUTE IMMEDIATE 'SELECT * FROM `exec.proj.tbl`'; END;",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "EXECUTE IMMEDIATE is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "table inside string literal should be ignored",
|
||||
sql: "SELECT * FROM `real.table.one` WHERE name = 'select * from `fake.table.two`'",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.one"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "string with escaped single quote",
|
||||
sql: "SELECT 'this is a string with an escaped quote \\' and a fake table `fake.table.one`' FROM `real.table.two`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.two"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "string with escaped double quote",
|
||||
sql: `SELECT "this is a string with an escaped quote \" and a fake table ` + "`fake.table.one`" + `" FROM ` + "`real.table.two`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.two"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multi-line comment",
|
||||
sql: "/* `fake.table.1` */ SELECT * FROM `real.table.2`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "raw string with backslash should be ignored",
|
||||
sql: "SELECT * FROM `real.table.one` WHERE name = r'a raw string with a \\ and a fake table `fake.table.two`'",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.one"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "capital R raw string with quotes inside should be ignored",
|
||||
sql: `SELECT * FROM ` + "`real.table.one`" + ` WHERE name = R"""a raw string with a ' and a " and a \ and a fake table ` + "`fake.table.two`" + `"""`,
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.one"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "triple quoted raw string should be ignored",
|
||||
sql: "SELECT * FROM `real.table.one` WHERE name = r'''a raw string with a ' and a \" and a \\ and a fake table `fake.table.two`'''",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.one"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "triple quoted capital R raw string should be ignored",
|
||||
sql: `SELECT * FROM ` + "`real.table.one`" + ` WHERE name = R"""a raw string with a ' and a " and a \ and a fake table ` + "`fake.table.two`" + `"""`,
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"real.table.one"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unquoted fully qualified table",
|
||||
sql: "SELECT * FROM my-project.my_dataset.my_table",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unquoted partial table with default project",
|
||||
sql: "SELECT * FROM my_dataset.my_table",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"default-proj.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unquoted partial table without default project",
|
||||
sql: "SELECT * FROM my_dataset.my_table",
|
||||
defaultProjectID: "",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "mixed quoting style 1",
|
||||
sql: "SELECT * FROM `my-project`.my_dataset.my_table",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed quoting style 2",
|
||||
sql: "SELECT * FROM `my-project`.`my_dataset`.my_table",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed quoting style 3",
|
||||
sql: "SELECT * FROM `my-project`.`my_dataset`.`my_table`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed quoted and unquoted tables",
|
||||
sql: "SELECT * FROM `proj1.data1.tbl1` JOIN proj2.data2.tbl2 ON id",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj1.data1.tbl1", "proj2.data2.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "create table statement",
|
||||
sql: "CREATE TABLE `my-project.my_dataset.my_table` (x INT64)",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "insert into statement",
|
||||
sql: "INSERT INTO `my-project.my_dataset.my_table` (x) VALUES (1)",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "update statement",
|
||||
sql: "UPDATE `my-project.my_dataset.my_table` SET x = 2 WHERE true",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete from statement",
|
||||
sql: "DELETE FROM `my-project.my_dataset.my_table` WHERE true",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"my-project.my_dataset.my_table"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "merge into statement",
|
||||
sql: "MERGE `proj.data.target` T USING `proj.data.source` S ON T.id = S.id WHEN NOT MATCHED THEN INSERT ROW",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj.data.source", "proj.data.target"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "create schema statement",
|
||||
sql: "CREATE SCHEMA `my-project.my_dataset`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "dataset-level operations like 'CREATE SCHEMA' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "create dataset statement",
|
||||
sql: "CREATE DATASET `my-project.my_dataset`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "dataset-level operations like 'CREATE DATASET' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "drop schema statement",
|
||||
sql: "DROP SCHEMA `my-project.my_dataset`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "dataset-level operations like 'DROP SCHEMA' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "drop dataset statement",
|
||||
sql: "DROP DATASET `my-project.my_dataset`",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "dataset-level operations like 'DROP DATASET' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "alter schema statement",
|
||||
sql: "ALTER SCHEMA my_dataset SET OPTIONS(description='new description')",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "dataset-level operations like 'ALTER SCHEMA' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "alter dataset statement",
|
||||
sql: "ALTER DATASET my_dataset SET OPTIONS(description='new description')",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "dataset-level operations like 'ALTER DATASET' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "begin...end block",
|
||||
sql: "BEGIN CREATE TABLE `proj.data.tbl1` (x INT64); INSERT `proj.data.tbl2` (y) VALUES (1); END;",
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj.data.tbl1", "proj.data.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex begin...end block with comments and different quoting",
|
||||
sql: `
|
||||
BEGIN
|
||||
-- Create a new table
|
||||
CREATE TABLE proj.data.tbl1 (x INT64);
|
||||
/* Insert some data from another table */
|
||||
INSERT INTO ` + "`proj.data.tbl2`" + ` (y) SELECT y FROM proj.data.source;
|
||||
END;`,
|
||||
defaultProjectID: "default-proj",
|
||||
want: []string{"proj.data.source", "proj.data.tbl1", "proj.data.tbl2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "call fully qualified procedure",
|
||||
sql: "CALL my-project.my_dataset.my_procedure()",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "CALL is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "call partially qualified procedure",
|
||||
sql: "CALL my_dataset.my_procedure()",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "CALL is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "call procedure in begin...end block",
|
||||
sql: "BEGIN CALL proj.data.proc1(); SELECT * FROM proj.data.tbl1; END;",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "CALL is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "call procedure with newline",
|
||||
sql: "CALL\nmy_dataset.my_procedure()",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "CALL is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "call procedure without default project should fail",
|
||||
sql: "CALL my_dataset.my_procedure()",
|
||||
defaultProjectID: "",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "CALL is not allowed when dataset restrictions are in place",
|
||||
},
|
||||
{
|
||||
name: "create procedure statement",
|
||||
sql: "CREATE PROCEDURE my_dataset.my_procedure() BEGIN SELECT 1; END;",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "unanalyzable statements like 'CREATE PROCEDURE' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "create or replace procedure statement",
|
||||
sql: "CREATE\n OR \nREPLACE \nPROCEDURE my_dataset.my_procedure() BEGIN SELECT 1; END;",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "unanalyzable statements like 'CREATE OR REPLACE PROCEDURE' are not allowed",
|
||||
},
|
||||
{
|
||||
name: "create function statement",
|
||||
sql: "CREATE FUNCTION my_dataset.my_function() RETURNS INT64 AS (1);",
|
||||
defaultProjectID: "default-proj",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrMsg: "unanalyzable statements like 'CREATE FUNCTION' are not allowed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := bigquerycommon.TableParser(tc.sql, tc.defaultProjectID)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("TableParser() error = %v, wantErr %v", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if tc.wantErr && tc.wantErrMsg != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErrMsg) {
|
||||
t.Errorf("TableParser() error = %v, want err containing %q", err, tc.wantErrMsg)
|
||||
}
|
||||
}
|
||||
// Sort slices to ensure comparison is order-independent.
|
||||
sort.Strings(got)
|
||||
sort.Strings(tc.want)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("TableParser() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
internal/tools/bigquery/bigquerycommon/util.go
Normal file
55
internal/tools/bigquery/bigquerycommon/util.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2025 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 bigquerycommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
bigqueryapi "cloud.google.com/go/bigquery"
|
||||
bigqueryrestapi "google.golang.org/api/bigquery/v2"
|
||||
)
|
||||
|
||||
// DryRunQuery performs a dry run of the SQL query to validate it and get metadata.
|
||||
func DryRunQuery(ctx context.Context, restService *bigqueryrestapi.Service, projectID string, location string, sql string, params []*bigqueryrestapi.QueryParameter, connProps []*bigqueryapi.ConnectionProperty) (*bigqueryrestapi.Job, error) {
|
||||
useLegacySql := false
|
||||
|
||||
restConnProps := make([]*bigqueryrestapi.ConnectionProperty, len(connProps))
|
||||
for i, prop := range connProps {
|
||||
restConnProps[i] = &bigqueryrestapi.ConnectionProperty{Key: prop.Key, Value: prop.Value}
|
||||
}
|
||||
|
||||
jobToInsert := &bigqueryrestapi.Job{
|
||||
JobReference: &bigqueryrestapi.JobReference{
|
||||
ProjectId: projectID,
|
||||
Location: location,
|
||||
},
|
||||
Configuration: &bigqueryrestapi.JobConfiguration{
|
||||
DryRun: true,
|
||||
Query: &bigqueryrestapi.JobConfigurationQuery{
|
||||
Query: sql,
|
||||
UseLegacySql: &useLegacySql,
|
||||
ConnectionProperties: restConnProps,
|
||||
QueryParameters: params,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
insertResponse, err := restService.Jobs.Insert(projectID, jobToInsert).Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert dry run job: %w", err)
|
||||
}
|
||||
return insertResponse, nil
|
||||
}
|
||||
@@ -149,12 +149,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
tableRefsParameter := tools.NewStringParameter("table_references", tableRefsDescription)
|
||||
|
||||
parameters := tools.Parameters{userQueryParameter, tableRefsParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// Get cloud-platform token source for Gemini Data Analytics API during initialization
|
||||
var bigQueryTokenSourceWithScope oauth2.TokenSource
|
||||
|
||||
@@ -18,12 +18,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
bigqueryapi "cloud.google.com/go/bigquery"
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
bigqueryds "github.com/googleapis/genai-toolbox/internal/sources/bigquery"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
bqutil "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerycommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
bigqueryrestapi "google.golang.org/api/bigquery/v2"
|
||||
"google.golang.org/api/iterator"
|
||||
@@ -50,6 +52,8 @@ type compatibleSource interface {
|
||||
BigQueryRestService() *bigqueryrestapi.Service
|
||||
BigQueryClientCreator() bigqueryds.BigqueryClientCreator
|
||||
UseClientAuthorization() bool
|
||||
IsDatasetAllowed(projectID, datasetID string) bool
|
||||
BigQueryAllowedDatasets() []string
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
@@ -85,7 +89,28 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
sqlParameter := tools.NewStringParameter("sql", "The sql to execute.")
|
||||
sqlDescription := "The sql to execute."
|
||||
allowedDatasets := s.BigQueryAllowedDatasets()
|
||||
if len(allowedDatasets) > 0 {
|
||||
datasetIDs := []string{}
|
||||
for _, ds := range allowedDatasets {
|
||||
datasetIDs = append(datasetIDs, fmt.Sprintf("`%s`", ds))
|
||||
}
|
||||
|
||||
if len(datasetIDs) == 1 {
|
||||
parts := strings.Split(allowedDatasets[0], ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("expected split to have 2 parts: %s", allowedDatasets[0])
|
||||
}
|
||||
datasetID := parts[1]
|
||||
sqlDescription += fmt.Sprintf(" The query must only access the %s dataset. "+
|
||||
"To query a table within this dataset (e.g., `my_table`), "+
|
||||
"qualify it with the dataset id (e.g., `%s.my_table`).", datasetIDs[0], datasetID)
|
||||
} else {
|
||||
sqlDescription += fmt.Sprintf(" The query must only access datasets from the following list: %s.", strings.Join(datasetIDs, ", "))
|
||||
}
|
||||
}
|
||||
sqlParameter := tools.NewStringParameter("sql", sqlDescription)
|
||||
dryRunParameter := tools.NewBooleanParameterWithDefault(
|
||||
"dry_run",
|
||||
false,
|
||||
@@ -93,25 +118,22 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
"will be returned without running the query. Defaults to false.",
|
||||
)
|
||||
parameters := tools.Parameters{sqlParameter, dryRunParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientAuthorization(),
|
||||
ClientCreator: s.BigQueryClientCreator(),
|
||||
Client: s.BigQueryClient(),
|
||||
RestService: s.BigQueryRestService(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientAuthorization(),
|
||||
ClientCreator: s.BigQueryClientCreator(),
|
||||
Client: s.BigQueryClient(),
|
||||
RestService: s.BigQueryRestService(),
|
||||
IsDatasetAllowed: s.IsDatasetAllowed,
|
||||
AllowedDatasets: allowedDatasets,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -126,11 +148,13 @@ type Tool struct {
|
||||
UseClientOAuth bool `yaml:"useClientOAuth"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
|
||||
Client *bigqueryapi.Client
|
||||
RestService *bigqueryrestapi.Service
|
||||
ClientCreator bigqueryds.BigqueryClientCreator
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
Client *bigqueryapi.Client
|
||||
RestService *bigqueryrestapi.Service
|
||||
ClientCreator bigqueryds.BigqueryClientCreator
|
||||
IsDatasetAllowed func(projectID, datasetID string) bool
|
||||
AllowedDatasets []string
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -160,10 +184,65 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
}
|
||||
}
|
||||
|
||||
dryRunJob, err := dryRunQuery(ctx, restService, bqClient.Project(), bqClient.Location, sql)
|
||||
dryRunJob, err := bqutil.DryRunQuery(ctx, restService, bqClient.Project(), bqClient.Location, sql, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query validation failed during dry run: %w", err)
|
||||
}
|
||||
statementType := dryRunJob.Statistics.Query.StatementType
|
||||
|
||||
if len(t.AllowedDatasets) > 0 {
|
||||
switch statementType {
|
||||
case "CREATE_SCHEMA", "DROP_SCHEMA", "ALTER_SCHEMA":
|
||||
return nil, fmt.Errorf("dataset-level operations like '%s' are not allowed when dataset restrictions are in place", statementType)
|
||||
case "CREATE_FUNCTION", "CREATE_TABLE_FUNCTION", "CREATE_PROCEDURE":
|
||||
return nil, fmt.Errorf("creating stored routines ('%s') is not allowed when dataset restrictions are in place, as their contents cannot be safely analyzed", statementType)
|
||||
case "CALL":
|
||||
return nil, fmt.Errorf("calling stored procedures ('%s') is not allowed when dataset restrictions are in place, as their contents cannot be safely analyzed", statementType)
|
||||
}
|
||||
|
||||
// Use a map to avoid duplicate table names.
|
||||
tableIDSet := make(map[string]struct{})
|
||||
|
||||
// Get all tables from the dry run result. This is the most reliable method.
|
||||
queryStats := dryRunJob.Statistics.Query
|
||||
if queryStats != nil {
|
||||
for _, tableRef := range queryStats.ReferencedTables {
|
||||
tableIDSet[fmt.Sprintf("%s.%s.%s", tableRef.ProjectId, tableRef.DatasetId, tableRef.TableId)] = struct{}{}
|
||||
}
|
||||
if tableRef := queryStats.DdlTargetTable; tableRef != nil {
|
||||
tableIDSet[fmt.Sprintf("%s.%s.%s", tableRef.ProjectId, tableRef.DatasetId, tableRef.TableId)] = struct{}{}
|
||||
}
|
||||
if tableRef := queryStats.DdlDestinationTable; tableRef != nil {
|
||||
tableIDSet[fmt.Sprintf("%s.%s.%s", tableRef.ProjectId, tableRef.DatasetId, tableRef.TableId)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var tableNames []string
|
||||
if len(tableIDSet) > 0 {
|
||||
for tableID := range tableIDSet {
|
||||
tableNames = append(tableNames, tableID)
|
||||
}
|
||||
} else if statementType != "SELECT" {
|
||||
// If dry run yields no tables, fall back to the parser for non-SELECT statements
|
||||
// to catch unsafe operations like EXECUTE IMMEDIATE.
|
||||
parsedTables, parseErr := bqutil.TableParser(sql, t.Client.Project())
|
||||
if parseErr != nil {
|
||||
// If parsing fails (e.g., EXECUTE IMMEDIATE), we cannot guarantee safety, so we must fail.
|
||||
return nil, fmt.Errorf("could not parse tables from query to validate against allowed datasets: %w", parseErr)
|
||||
}
|
||||
tableNames = parsedTables
|
||||
}
|
||||
|
||||
for _, tableID := range tableNames {
|
||||
parts := strings.Split(tableID, ".")
|
||||
if len(parts) == 3 {
|
||||
projectID, datasetID := parts[0], parts[1]
|
||||
if !t.IsDatasetAllowed(projectID, datasetID) {
|
||||
return nil, fmt.Errorf("query accesses dataset '%s.%s', which is not in the allowed list", projectID, datasetID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
if dryRunJob != nil {
|
||||
@@ -177,8 +256,6 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return "Dry run was requested, but no job information was returned.", nil
|
||||
}
|
||||
|
||||
statementType := dryRunJob.Statistics.Query.StatementType
|
||||
// JobStatistics.QueryStatistics.StatementType
|
||||
query := bqClient.Query(sql)
|
||||
query.Location = bqClient.Location
|
||||
|
||||
@@ -248,27 +325,3 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
// dryRunQuery performs a dry run of the SQL query to validate it and get metadata.
|
||||
func dryRunQuery(ctx context.Context, restService *bigqueryrestapi.Service, projectID string, location string, sql string) (*bigqueryrestapi.Job, error) {
|
||||
useLegacySql := false
|
||||
jobToInsert := &bigqueryrestapi.Job{
|
||||
JobReference: &bigqueryrestapi.JobReference{
|
||||
ProjectId: projectID,
|
||||
Location: location,
|
||||
},
|
||||
Configuration: &bigqueryrestapi.JobConfiguration{
|
||||
DryRun: true,
|
||||
Query: &bigqueryrestapi.JobConfigurationQuery{
|
||||
Query: sql,
|
||||
UseLegacySql: &useLegacySql,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
insertResponse, err := restService.Jobs.Insert(projectID, jobToInsert).Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert dry run job: %w", err)
|
||||
}
|
||||
return insertResponse, nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
bigqueryds "github.com/googleapis/genai-toolbox/internal/sources/bigquery"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
bqutil "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerycommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
bigqueryrestapi "google.golang.org/api/bigquery/v2"
|
||||
"google.golang.org/api/iterator"
|
||||
@@ -50,6 +51,8 @@ type compatibleSource interface {
|
||||
BigQueryRestService() *bigqueryrestapi.Service
|
||||
BigQueryClientCreator() bigqueryds.BigqueryClientCreator
|
||||
UseClientAuthorization() bool
|
||||
IsDatasetAllowed(projectID, datasetID string) bool
|
||||
BigQueryAllowedDatasets() []string
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
@@ -85,8 +88,17 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
historyDataParameter := tools.NewStringParameter("history_data",
|
||||
"The table id or the query of the history time series data.")
|
||||
allowedDatasets := s.BigQueryAllowedDatasets()
|
||||
historyDataDescription := "The table id or the query of the history time series data."
|
||||
if len(allowedDatasets) > 0 {
|
||||
datasetIDs := []string{}
|
||||
for _, ds := range allowedDatasets {
|
||||
datasetIDs = append(datasetIDs, fmt.Sprintf("`%s`", ds))
|
||||
}
|
||||
historyDataDescription += fmt.Sprintf(" The query or table must only access datasets from the following list: %s.", strings.Join(datasetIDs, ", "))
|
||||
}
|
||||
|
||||
historyDataParameter := tools.NewStringParameter("history_data", historyDataDescription)
|
||||
timestampColumnNameParameter := tools.NewStringParameter("timestamp_col",
|
||||
"The name of the time series timestamp column.")
|
||||
dataColumnNameParameter := tools.NewStringParameter("data_col",
|
||||
@@ -98,24 +110,22 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
parameters := tools.Parameters{historyDataParameter,
|
||||
timestampColumnNameParameter, dataColumnNameParameter, idColumnNameParameter, horizonParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientAuthorization(),
|
||||
ClientCreator: s.BigQueryClientCreator(),
|
||||
Client: s.BigQueryClient(),
|
||||
RestService: s.BigQueryRestService(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientAuthorization(),
|
||||
ClientCreator: s.BigQueryClientCreator(),
|
||||
Client: s.BigQueryClient(),
|
||||
RestService: s.BigQueryRestService(),
|
||||
IsDatasetAllowed: s.IsDatasetAllowed,
|
||||
AllowedDatasets: allowedDatasets,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -130,11 +140,13 @@ type Tool struct {
|
||||
UseClientOAuth bool `yaml:"useClientOAuth"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
|
||||
Client *bigqueryapi.Client
|
||||
RestService *bigqueryrestapi.Service
|
||||
ClientCreator bigqueryds.BigqueryClientCreator
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
Client *bigqueryapi.Client
|
||||
RestService *bigqueryrestapi.Service
|
||||
ClientCreator bigqueryds.BigqueryClientCreator
|
||||
IsDatasetAllowed func(projectID, datasetID string) bool
|
||||
AllowedDatasets []string
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -175,8 +187,48 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
var historyDataSource string
|
||||
trimmedUpperHistoryData := strings.TrimSpace(strings.ToUpper(historyData))
|
||||
if strings.HasPrefix(trimmedUpperHistoryData, "SELECT") || strings.HasPrefix(trimmedUpperHistoryData, "WITH") {
|
||||
if len(t.AllowedDatasets) > 0 {
|
||||
dryRunJob, err := bqutil.DryRunQuery(ctx, t.RestService, t.Client.Project(), t.Client.Location, historyData, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query validation failed during dry run: %w", err)
|
||||
}
|
||||
statementType := dryRunJob.Statistics.Query.StatementType
|
||||
if statementType != "SELECT" {
|
||||
return nil, fmt.Errorf("the 'history_data' parameter only supports a table ID or a SELECT query. The provided query has statement type '%s'", statementType)
|
||||
}
|
||||
|
||||
queryStats := dryRunJob.Statistics.Query
|
||||
if queryStats != nil {
|
||||
for _, tableRef := range queryStats.ReferencedTables {
|
||||
if !t.IsDatasetAllowed(tableRef.ProjectId, tableRef.DatasetId) {
|
||||
return nil, fmt.Errorf("query in history_data accesses dataset '%s.%s', which is not in the allowed list", tableRef.ProjectId, tableRef.DatasetId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not analyze query in history_data to validate against allowed datasets")
|
||||
}
|
||||
}
|
||||
historyDataSource = fmt.Sprintf("(%s)", historyData)
|
||||
} else {
|
||||
if len(t.AllowedDatasets) > 0 {
|
||||
parts := strings.Split(historyData, ".")
|
||||
var projectID, datasetID string
|
||||
|
||||
switch len(parts) {
|
||||
case 3: // project.dataset.table
|
||||
projectID = parts[0]
|
||||
datasetID = parts[1]
|
||||
case 2: // dataset.table
|
||||
projectID = t.Client.Project()
|
||||
datasetID = parts[0]
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid table ID format for 'history_data': %q. Expected 'dataset.table' or 'project.dataset.table'", historyData)
|
||||
}
|
||||
|
||||
if !t.IsDatasetAllowed(projectID, datasetID) {
|
||||
return nil, fmt.Errorf("access to dataset '%s.%s' (from table '%s') is not allowed", projectID, datasetID, historyData)
|
||||
}
|
||||
}
|
||||
historyDataSource = fmt.Sprintf("TABLE `%s`", historyData)
|
||||
}
|
||||
|
||||
|
||||
@@ -87,11 +87,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
datasetParameter := tools.NewStringParameter(datasetKey, "The dataset to get metadata information.")
|
||||
parameters := tools.Parameters{projectParameter, datasetParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -89,11 +89,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
tableParameter := tools.NewStringParameter(tableKey, "The table to get metadata information.")
|
||||
parameters := tools.Parameters{projectParameter, datasetParameter, tableParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -87,11 +87,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
parameters := tools.Parameters{projectParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -128,11 +128,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
parameters := tools.Parameters{projectParameter, datasetParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -96,12 +96,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
if cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, parameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
bigqueryds "github.com/googleapis/genai-toolbox/internal/sources/bigquery"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
bqutil "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerycommon"
|
||||
bigqueryrestapi "google.golang.org/api/bigquery/v2"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
@@ -89,16 +90,12 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
@@ -236,7 +233,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
query.Parameters = highLevelParams
|
||||
query.Location = bqClient.Location
|
||||
|
||||
dryRunJob, err := dryRunQuery(ctx, restService, bqClient.Project(), bqClient.Location, newStatement, lowLevelParams, query.ConnectionProperties)
|
||||
dryRunJob, err := bqutil.DryRunQuery(ctx, restService, bqClient.Project(), bqClient.Location, newStatement, lowLevelParams, query.ConnectionProperties)
|
||||
if err != nil {
|
||||
// This is a fallback check in case the switch logic was bypassed.
|
||||
return nil, fmt.Errorf("final query validation failed: %w", err)
|
||||
@@ -319,42 +316,3 @@ func BQTypeStringFromToolType(toolType string) (string, error) {
|
||||
return "", fmt.Errorf("unsupported tool parameter type for BigQuery: %s", toolType)
|
||||
}
|
||||
}
|
||||
|
||||
func dryRunQuery(
|
||||
ctx context.Context,
|
||||
restService *bigqueryrestapi.Service,
|
||||
projectID string,
|
||||
location string,
|
||||
sql string,
|
||||
params []*bigqueryrestapi.QueryParameter,
|
||||
connProps []*bigqueryapi.ConnectionProperty,
|
||||
) (*bigqueryrestapi.Job, error) {
|
||||
useLegacySql := false
|
||||
|
||||
restConnProps := make([]*bigqueryrestapi.ConnectionProperty, len(connProps))
|
||||
for i, prop := range connProps {
|
||||
restConnProps[i] = &bigqueryrestapi.ConnectionProperty{Key: prop.Key, Value: prop.Value}
|
||||
}
|
||||
|
||||
jobToInsert := &bigqueryrestapi.Job{
|
||||
JobReference: &bigqueryrestapi.JobReference{
|
||||
ProjectId: projectID,
|
||||
Location: location,
|
||||
},
|
||||
Configuration: &bigqueryrestapi.JobConfiguration{
|
||||
DryRun: true,
|
||||
Query: &bigqueryrestapi.JobConfigurationQuery{
|
||||
Query: sql,
|
||||
UseLegacySql: &useLegacySql,
|
||||
ConnectionProperties: restConnProps,
|
||||
QueryParameters: params,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
insertResponse, err := restService.Jobs.Insert(projectID, jobToInsert).Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert dry run job: %w", err)
|
||||
}
|
||||
return insertResponse, nil
|
||||
}
|
||||
|
||||
@@ -81,16 +81,12 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -74,16 +74,12 @@ func (c Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, err := tools.ProcessParameters(c.TemplateParameters, c.Parameters)
|
||||
allParameters, paramManifest, err := tools.ProcessParameters(c.TemplateParameters, c.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(c.Name, c.Description, c.AuthRequired, allParameters)
|
||||
|
||||
t := Tool{
|
||||
Name: c.Name,
|
||||
|
||||
@@ -74,11 +74,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
sqlParameter := tools.NewStringParameter("sql", "The SQL statement to execute.")
|
||||
parameters := tools.Parameters{sqlParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
t := ExecuteSQLTool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -72,13 +72,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", listDatabasesKind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, _ := tools.ProcessParameters(nil, cfg.Parameters)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
allParameters, paramManifest, _ := tools.ProcessParameters(nil, cfg.Parameters)
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -76,13 +76,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
databaseParameter := tools.NewStringParameter(databaseKey, "The database to list tables from.")
|
||||
parameters := tools.Parameters{databaseParameter}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, _ := tools.ProcessParameters(nil, parameters)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
allParameters, paramManifest, _ := tools.ProcessParameters(nil, parameters)
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -74,13 +74,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", sqlKind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, _ := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
allParameters, paramManifest, _ := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -77,6 +77,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
tools.NewStringParameterWithRequired("query", "The promql query to execute.", true),
|
||||
}
|
||||
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
@@ -86,7 +88,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
UserAgent: s.UserAgent,
|
||||
Client: s.Client,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: allParameters.Manifest()},
|
||||
mcpManifest: tools.McpManifest{Name: cfg.Name, Description: cfg.Description, InputSchema: allParameters.McpManifest()},
|
||||
mcpManifest: mcpManifest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -76,19 +76,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "instance", "name"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a new database in a Cloud SQL instance."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -78,18 +78,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a new user in a Cloud SQL instance. Both built-in and IAM users are supported. IAM users require an email account as the user name. IAM is the more secure and recommended way to manage users. The agent should always ask the user what type of user they want to create. For more information, see https://cloud.google.com/sql/docs/postgres/add-manage-iam-users"
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -75,19 +75,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"projectId", "instanceId"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Gets a particular cloud sql instance."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -74,19 +74,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "instance"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Lists all databases for a Cloud SQL instance."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -73,19 +73,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Lists all type of Cloud SQL instances for a project."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -128,19 +128,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "operation"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "This will poll on operations API until the operation is done. For checking operation status we need projectId and operationId. Once instance is created give follow up steps on how to use the variables to bring data plane MCP server up in local and remote setup."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
var delay time.Duration
|
||||
if cfg.Delay == "" {
|
||||
|
||||
@@ -79,19 +79,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "name", "editionPreset", "rootPassword"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a SQL Server instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 8 GiB RAM (`db-custom-2-8192`) configuration with Non-HA/zonal availability. For the `Production` template, it chooses a 4 vCPU, 26 GiB RAM (`db-custom-4-26624`) configuration with HA/regional availability. The Enterprise edition is used in both cases. The default database version is `SQLSERVER_2022_STANDARD`. The agent should ask the user if they want to use a different version."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -79,19 +79,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "name", "editionPreset", "rootPassword"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a MySQL instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 16 GiB RAM, 100 GiB SSD configuration with Non-HA/zonal availability. For the `Production` template, it chooses an 8 vCPU, 64 GiB RAM, 250 GiB SSD configuration with HA/regional availability. The Enterprise Plus edition is used in both cases. The default database version is `MYSQL_8_4`. The agent should ask the user if they want to use a different version."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -79,19 +79,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "name", "editionPreset", "rootPassword"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a Postgres instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 16 GiB RAM, 100 GiB SSD configuration with Non-HA/zonal availability. For the `Production` template, it chooses an 8 vCPU, 64 GiB RAM, 250 GiB SSD configuration with HA/regional availability. The Enterprise Plus edition is used in both cases. The default database version is `POSTGRES_17`. The agent should ask the user if they want to use a different version."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -83,16 +83,12 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -100,11 +100,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
entry := tools.NewStringParameter("entry", "The resource name of the Entry in the following form: projects/{project}/locations/{location}/entryGroups/{entryGroup}/entries/{entry}.")
|
||||
parameters := tools.Parameters{name, view, aspectTypes, entry}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -85,11 +85,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
orderBy := tools.NewStringParameterWithDefault("orderBy", "relevance", "Specifies the ordering of results. Supported values are: relevance, last_modified_timestamp, last_modified_timestamp asc")
|
||||
parameters := tools.Parameters{query, pageSize, orderBy}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -84,11 +84,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
orderBy := tools.NewStringParameterWithDefault("orderBy", "relevance", "Specifies the ordering of results. Supported values are: relevance, last_modified_timestamp, last_modified_timestamp asc")
|
||||
parameters := tools.Parameters{query, pageSize, orderBy}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
|
||||
@@ -82,11 +82,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: cfg.Parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, cfg.Parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -78,23 +78,14 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
sqlParameter := tools.NewStringParameter("sql", "The sql to execute.")
|
||||
parameters := tools.Parameters{sqlParameter}
|
||||
|
||||
_, paramManifest, paramMcpManifest, err := tools.ProcessParameters(nil, parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
t := &Tool{
|
||||
Name: cfg.Name,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Db: s.FirebirdDB(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
|
||||
@@ -82,16 +82,12 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
allParameters, paramManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
// finish tool setup
|
||||
t := &Tool{
|
||||
|
||||
@@ -117,11 +117,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
returnDataParameter,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -83,11 +83,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to delete from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path"))
|
||||
parameters := tools.Parameters{documentPathsParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -83,11 +83,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to retrieve from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path"))
|
||||
parameters := tools.Parameters{documentPathsParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -83,11 +83,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// No parameters needed for this tool
|
||||
parameters := tools.Parameters{}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -84,11 +84,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
parentPathParameter := tools.NewStringParameterWithDefault(parentPathKey, emptyString, "Relative parent document path to list subcollections from (e.g., 'users/userId'). If not provided, lists root collections. Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'")
|
||||
parameters := tools.Parameters{parentPathParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -131,11 +131,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: cfg.Parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, cfg.Parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -130,11 +130,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create parameters
|
||||
parameters := createParameters()
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -127,11 +127,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
returnDataParameter,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -87,12 +87,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// Create parameters
|
||||
parameters := createParameters()
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
|
||||
@@ -108,11 +108,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
|
||||
@@ -85,11 +85,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
)
|
||||
parameters = append(parameters, vizParameter)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
|
||||
@@ -284,3 +284,29 @@ func RunInlineQuery2(l *v4.LookerSDK, request RequestRunInlineQuery2, options *r
|
||||
err := l.AuthSession.Do(&result, "POST", "/4.0", "/queries/run_inline", nil, request, options)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func RunInlineQuery(ctx context.Context, sdk *v4.LookerSDK, wq *v4.WriteQuery, format string, options *rtl.ApiSettings) (string, error) {
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get logger from ctx: %s", err)
|
||||
}
|
||||
req := v4.RequestRunInlineQuery{
|
||||
Body: *wq,
|
||||
ResultFormat: format,
|
||||
}
|
||||
req2 := RequestRunInlineQuery2{
|
||||
Query: *wq,
|
||||
RenderOpts: RenderOptions{
|
||||
Format: format,
|
||||
},
|
||||
QueryApiClientCtx: QueryApiClientContext{
|
||||
Name: "MCP Toolbox",
|
||||
},
|
||||
}
|
||||
resp, err := RunInlineQuery2(sdk, req2, options)
|
||||
if err != nil {
|
||||
logger.DebugContext(ctx, "error querying with new endpoint, trying again with original", err)
|
||||
resp, err = sdk.RunInlineQuery(req, options)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
@@ -83,11 +83,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
offsetParameter,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
|
||||
@@ -74,11 +74,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
parameters := lookercommon.GetFieldParameters()
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, parameters)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user