Compare commits

..

4 Commits

Author SHA1 Message Date
Alex Talreja
decff260fb feat: run tool button 2025-07-11 05:44:36 +00:00
Alex Talreja
dd61496420 create skeleton landing page 2025-07-09 22:02:39 +00:00
Alex Talreja
9cd494b18a resolve comments 2025-07-09 21:57:53 +00:00
Alex Talreja
005a1e624c feat: launch web server with --web-ui flag 2025-07-09 21:57:48 +00:00
83 changed files with 1354 additions and 3275 deletions

View File

@@ -2,7 +2,6 @@ assign_issues:
- kurtisvg
- Yuan325
- duwenxin99
- akitsch
assign_issues_by:
- labels:
- 'product: bigquery'
@@ -14,4 +13,3 @@ assign_prs:
- kurtisvg
- Yuan325
- duwenxin99
- akitsch

View File

@@ -20,7 +20,6 @@ extraFiles: [
"README.md",
"docs/en/getting-started/introduction/_index.md",
"docs/en/getting-started/local_quickstart.md",
"docs/en/getting-started/local_quickstart_js.md",
"docs/en/getting-started/mcp_quickstart/_index.md",
"docs/en/samples/bigquery/local_quickstart.md",
"docs/en/samples/bigquery/mcp_quickstart/_index.md",

View File

@@ -51,6 +51,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo

View File

@@ -1,2 +0,0 @@
{{ $file := .Get 0 }}
{{ (printf "%s%s" .Page.File.Dir $file) | readFile | replaceRE "^---[\\s\\S]+?---" "" | safeHTML }}

View File

@@ -1,22 +1,5 @@
# Changelog
## [0.9.0](https://github.com/googleapis/genai-toolbox/compare/v0.8.0...v0.9.0) (2025-07-11)
### Features
* Dynamic reloading for toolbox config ([#800](https://github.com/googleapis/genai-toolbox/issues/800)) ([4c240ac](https://github.com/googleapis/genai-toolbox/commit/4c240ac3c961cd14738c998ba2d10d5235ef523e))
* **sources/mysql:** Add queryTimeout support to MySQL source ([#830](https://github.com/googleapis/genai-toolbox/issues/830)) ([391cb5b](https://github.com/googleapis/genai-toolbox/commit/391cb5bfe845e554411240a1d9838df5331b25fa))
* **tools/bigquery:** Add optional projectID parameter to bigquery tools ([#799](https://github.com/googleapis/genai-toolbox/issues/799)) ([c6ab74c](https://github.com/googleapis/genai-toolbox/commit/c6ab74c5dad53a0e7885a18438ab3be36b9b7cb3))
### Bug Fixes
* Cleanup unassigned err log ([#857](https://github.com/googleapis/genai-toolbox/issues/857)) ([c081ace](https://github.com/googleapis/genai-toolbox/commit/c081ace46bb24cb3fd2adb21d519489be0d3f3c3))
* Fix docs preview deployment pipeline ([#787](https://github.com/googleapis/genai-toolbox/issues/787)) ([0a93b04](https://github.com/googleapis/genai-toolbox/commit/0a93b0482c8d3c64b324e67408d408f5576ecaf3))
* **tools:** Nil parameter error when arrays are used ([#801](https://github.com/googleapis/genai-toolbox/issues/801)) ([2bdcc08](https://github.com/googleapis/genai-toolbox/commit/2bdcc0841ab37d18e2f0d6fe63fb6f10da3e302b))
* Trigger reload on additional fsnotify operations ([#854](https://github.com/googleapis/genai-toolbox/issues/854)) ([aa8dbec](https://github.com/googleapis/genai-toolbox/commit/aa8dbec97095cf0d7ac771c8084a84e2d3d8ce4e))
## [0.8.0](https://github.com/googleapis/genai-toolbox/compare/v0.7.0...v0.8.0) (2025-07-02)

283
README.md
View File

@@ -2,9 +2,7 @@
# MCP Toolbox for Databases
[![Docs](https://img.shields.io/badge/docs-MCP_Toolbox-blue)](https://googleapis.github.io/genai-toolbox/)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/Dmm69peqjh)
[![Medium](https://img.shields.io/badge/Medium-12100E?style=flat&logo=medium&logoColor=white)](https://medium.com/@mcp_toolbox)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/Dmm69peqjh)
[![Go Report Card](https://goreportcard.com/badge/github.com/googleapis/genai-toolbox)](https://goreportcard.com/report/github.com/googleapis/genai-toolbox)
> [!NOTE]
@@ -40,7 +38,6 @@ documentation](https://googleapis.github.io/genai-toolbox/).
- [Toolsets](#toolsets)
- [Versioning](#versioning)
- [Contributing](#contributing)
- [Community](#community)
<!-- /TOC -->
@@ -114,7 +111,7 @@ To install Toolbox as a binary:
<!-- {x-release-please-start-version} -->
```sh
# see releases page for other versions
export VERSION=0.9.0
export VERSION=0.8.0
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
```
@@ -127,7 +124,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.9.0
export VERSION=0.8.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -140,7 +137,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.9.0
go install github.com/googleapis/genai-toolbox@v0.8.0
```
<!-- {x-release-please-end} -->
@@ -373,273 +370,7 @@ For more detailed instructions on using the Toolbox Core SDK, see the
</details>
</details>
</blockquote>
<details>
<summary>Go (<a href="https://github.com/googleapis/mcp-toolbox-sdk-go">Github</a>)</summary>
<br>
<blockquote>
<details open>
<summary>Core</summary>
1. Install [Toolbox Go SDK][toolbox-go]:
```bash
go get github.com/googleapis/mcp-toolbox-sdk-go
```
1. Load tools:
```go
package main
import (
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"context"
)
func main() {
// Make sure to add the error checks
// update the url to point to your server
URL := "http://127.0.0.1:5000";
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
// Framework agnostic tools
tools, err := client.LoadToolset("toolsetName", ctx)
}
```
For more detailed instructions on using the Toolbox Go SDK, see the
[project's README][toolbox-core-go-readme].
[toolbox-go]: https://pkg.go.dev/github.com/googleapis/mcp-toolbox-sdk-go/core
[toolbox-core-go-readme]: https://github.com/googleapis/mcp-toolbox-sdk-go/blob/main/core/README.md
</details>
<details>
<summary>LangChain Go</summary>
1. Install [Toolbox Go SDK][toolbox-go]:
```bash
go get github.com/googleapis/mcp-toolbox-sdk-go
```
2. Load tools:
```go
package main
import (
"context"
"encoding/json"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"github.com/tmc/langchaingo/llms"
)
func main() {
// Make sure to add the error checks
// update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
var paramsSchema map[string]any
_ = json.Unmarshal(inputschema, &paramsSchema)
// Use this tool with LangChainGo
langChainTool := llms.Tool{
Type: "function",
Function: &llms.FunctionDefinition{
Name: tool.Name(),
Description: tool.Description(),
Parameters: paramsSchema,
},
}
}
```
</details>
<details>
<summary>Genkit</summary>
1. Install [Toolbox Go SDK][toolbox-go]:
```bash
go get github.com/googleapis/mcp-toolbox-sdk-go
```
2. Load tools:
```go
package main
import (
"context"
"encoding/json"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"github.com/invopop/jsonschema"
)
func main() {
// Make sure to add the error checks
// Update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
g, err := genkit.Init(ctx)
client, err := core.NewToolboxClient(URL)
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
var schema *jsonschema.Schema
_ = json.Unmarshal(inputschema, &schema)
executeFn := func(ctx *ai.ToolContext, input any) (string, error) {
result, err := tool.Invoke(ctx, input.(map[string]any))
if err != nil {
// Propagate errors from the tool invocation.
return "", err
}
return result.(string), nil
}
// Use this tool with Genkit Go
genkitTool := genkit.DefineToolWithInputSchema(
g,
tool.Name(),
tool.Description(),
schema,
executeFn,
)
}
```
</details>
<details>
<summary>Go GenAI</summary>
1. Install [Toolbox Go SDK][toolbox-go]:
```bash
go get github.com/googleapis/mcp-toolbox-sdk-go
```
2. Load tools:
```go
package main
import (
"context"
"encoding/json"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"google.golang.org/genai"
)
func main() {
// Make sure to add the error checks
// Update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
var schema *genai.Schema
_ = json.Unmarshal(inputschema, &schema)
funcDeclaration := &genai.FunctionDeclaration{
Name: tool.Name(),
Description: tool.Description(),
Parameters: schema,
}
// Use this tool with Go GenAI
genAITool := &genai.Tool{
FunctionDeclarations: []*genai.FunctionDeclaration{funcDeclaration},
}
}
```
</details>
<details>
<summary>OpenAI Go</summary>
1. Install [Toolbox Go SDK][toolbox-go]:
```bash
go get github.com/googleapis/mcp-toolbox-sdk-go
```
2. Load tools:
```go
package main
import (
"context"
"encoding/json"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
openai "github.com/openai/openai-go"
)
func main() {
// Make sure to add the error checks
// Update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
var paramsSchema openai.FunctionParameters
_ = json.Unmarshal(inputschema, &paramsSchema)
// Use this tool with OpenAI Go
openAITool := openai.ChatCompletionToolParam{
Function: openai.FunctionDefinitionParam{
Name: tool.Name(),
Description: openai.String(tool.Description()),
Parameters: paramsSchema,
},
}
}
```
</details>
</details>
</blockquote>
</details>
## Configuration
@@ -738,7 +469,3 @@ to get started.
Please note that this project is released with a Contributor Code of Conduct.
By participating in this project you agree to abide by its terms. See
[Contributor Code of Conduct](CODE_OF_CONDUCT.md) for more information.
## Community
Join our [discord community](https://discord.gg/GQrFB3Ec3W) to connect with our developers!

View File

@@ -64,7 +64,6 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannerexecutesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannersql"
_ "github.com/googleapis/genai-toolbox/internal/tools/sqlitesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/wait"
_ "github.com/googleapis/genai-toolbox/internal/tools/valkey"
"github.com/spf13/cobra"
@@ -186,6 +185,7 @@ func NewCommand(opts ...Option) *Command {
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", "Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. Allowed: 'alloydb-postgres', 'bigquery', 'cloud-sql-mysql', 'cloud-sql-postgres', 'cloud-sql-mssql', 'postgres', 'spanner', 'spanner-postgres'.")
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
// wrap RunE command so that we have access to original Command object
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
@@ -489,14 +489,13 @@ func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles m
return
}
// only check for events which indicate user saved a new tools file
// multiple operations checked due to various file update methods across editors
if !e.Has(fsnotify.Write | fsnotify.Create | fsnotify.Rename) {
// only check for write events which indicate user saved a new tools file
if !e.Has(fsnotify.Write) {
continue
}
cleanedFilename := filepath.Clean(e.Name)
logger.DebugContext(ctx, fmt.Sprintf("%s event detected in %s", e.Op, cleanedFilename))
logger.DebugContext(ctx, fmt.Sprintf("WRITE event detected in %s", cleanedFilename))
folderChanged := watchingFolder &&
(strings.HasSuffix(cleanedFilename, ".yaml") || strings.HasSuffix(cleanedFilename, ".yml"))
@@ -726,6 +725,11 @@ func run(cmd *Command) error {
cmd.logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` instead")
cmd.cfg.AuthServiceConfigs = authSourceConfigs
}
if err != nil {
errMsg := fmt.Errorf("unable to parse tool file at %q: %w", cmd.tools_file, err)
cmd.logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
instrumentation, err := telemetry.CreateTelemetryInstrumentation(versionString)
if err != nil {
@@ -762,6 +766,9 @@ func run(cmd *Command) error {
return errMsg
}
cmd.logger.InfoContext(ctx, "Server ready to serve!")
if cmd.cfg.UI {
cmd.logger.InfoContext(ctx, "Toolbox UI is up and running at: http://localhost:5000/ui")
}
go func() {
defer close(srvErr)

View File

@@ -1152,8 +1152,7 @@ func TestSingleEdit(t *testing.T) {
t.Fatalf("error writing to file: %v", err)
}
// only check substring of DEBUG message due to some OS/editors firing different operations
detectedFileChange := regexp.MustCompile(fmt.Sprintf(`event detected in %s"`, regexEscapedPathFile))
detectedFileChange := regexp.MustCompile(fmt.Sprintf(`DEBUG "WRITE event detected in %s"`, regexEscapedPathFile))
_, err = testutils.WaitForString(ctx, detectedFileChange, pr)
if err != nil {
t.Fatalf("timeout or error waiting for file to detect write: %s", err)

View File

@@ -1 +1 @@
0.9.0
0.8.0

View File

@@ -190,18 +190,6 @@
"!sudo lsof -i :5432"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Optional: Enable Vertex AI API for Google Cloud\n",
"\n",
"If you're using a model hosted on **Vertex AI**, run the following command to enable the API:\n",
"\n",
"```bash\n",
"!gcloud services enable aiplatform.googleapis.com\n"
]
},
{
"cell_type": "markdown",
"metadata": {
@@ -234,7 +222,7 @@
},
"outputs": [],
"source": [
"version = \"0.9.0\" # x-release-please-version\n",
"version = \"0.8.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -86,7 +86,7 @@ To install Toolbox as a binary:
```sh
# see releases page for other versions
export VERSION=0.9.0
export VERSION=0.8.0
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
```
@@ -97,7 +97,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.9.0
export VERSION=0.8.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -108,7 +108,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.9.0
go install github.com/googleapis/genai-toolbox@v0.8.0
```
{{% /tab %}}
@@ -316,273 +316,4 @@ const tools = toolboxTools.map(getTool);
{{< /tabpane >}}
For more detailed instructions on using the Toolbox Core SDK, see the
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-js/blob/main/packages/toolbox-core/README.md).
#### Go
Once you've installed the [Toolbox Go
SDK](https://pkg.go.dev/github.com/googleapis/mcp-toolbox-sdk-go/core), you can load
tools:
{{< tabpane text=true persist=header >}}
{{% tab header="Core" lang="en" %}}
{{< highlight go >}}
package main
import (
"context"
"log"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
)
func main() {
// update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
if err != nil {
log.Fatalf("Failed to create Toolbox client: %v", err)
}
// Framework agnostic tools
tools, err := client.LoadToolset("toolsetName", ctx)
if err != nil {
log.Fatalf("Failed to load tools: %v", err)
}
}
{{< /highlight >}}
{{% /tab %}}
{{% tab header="LangChain Go" lang="en" %}}
{{< highlight go >}}
package main
import (
"context"
"encoding/json"
"log"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"github.com/tmc/langchaingo/llms"
)
func main() {
// Make sure to add the error checks
// update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
if err != nil {
log.Fatalf("Failed to create Toolbox client: %v", err)
}
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
if err != nil {
log.Fatalf("Failed to load tools: %v", err)
}
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
if err != nil {
log.Fatalf("Failed to fetch inputSchema: %v", err)
}
var paramsSchema map[string]any
_ = json.Unmarshal(inputschema, &paramsSchema)
// Use this tool with LangChainGo
langChainTool := llms.Tool{
Type: "function",
Function: &llms.FunctionDefinition{
Name: tool.Name(),
Description: tool.Description(),
Parameters: paramsSchema,
},
}
}
{{< /highlight >}}
{{% /tab %}}
{{% tab header="Genkit Go" lang="en" %}}
{{< highlight go >}}
package main
import (
"context"
"encoding/json"
"log"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"github.com/invopop/jsonschema"
)
func main() {
// Make sure to add the error checks
// Update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
g, err := genkit.Init(ctx)
client, err := core.NewToolboxClient(URL)
if err != nil {
log.Fatalf("Failed to create Toolbox client: %v", err)
}
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
if err != nil {
log.Fatalf("Failed to load tools: %v", err)
}
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
if err != nil {
log.Fatalf("Failed to fetch inputSchema: %v", err)
}
var schema *jsonschema.Schema
_ = json.Unmarshal(inputschema, &schema)
executeFn := func(ctx *ai.ToolContext, input any) (string, error) {
result, err := tool.Invoke(ctx, input.(map[string]any))
if err != nil {
// Propagate errors from the tool invocation.
return "", err
}
return result.(string), nil
}
// Use this tool with Genkit Go
genkitTool := genkit.DefineToolWithInputSchema(
g,
tool.Name(),
tool.Description(),
schema,
executeFn,
)
}
{{< /highlight >}}
{{% /tab %}}
{{% tab header="Go GenAI" lang="en" %}}
{{< highlight go >}}
package main
import (
"context"
"encoding/json"
"log"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"google.golang.org/genai"
)
func main() {
// Make sure to add the error checks
// Update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
if err != nil {
log.Fatalf("Failed to create Toolbox client: %v", err)
}
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
if err != nil {
log.Fatalf("Failed to load tools: %v", err)
}
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
if err != nil {
log.Fatalf("Failed to fetch inputSchema: %v", err)
}
var schema *genai.Schema
_ = json.Unmarshal(inputschema, &schema)
funcDeclaration := &genai.FunctionDeclaration{
Name: tool.Name(),
Description: tool.Description(),
Parameters: schema,
}
// Use this tool with Go GenAI
genAITool := &genai.Tool{
FunctionDeclarations: []*genai.FunctionDeclaration{funcDeclaration},
}
}
{{< /highlight >}}
{{% /tab %}}
{{% tab header="OpenAI Go" lang="en" %}}
{{< highlight go >}}
package main
import (
"context"
"encoding/json"
"log"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
openai "github.com/openai/openai-go"
)
func main() {
// Make sure to add the error checks
// Update the url to point to your server
URL := "http://127.0.0.1:5000"
ctx := context.Background()
client, err := core.NewToolboxClient(URL)
if err != nil {
log.Fatalf("Failed to create Toolbox client: %v", err)
}
// Framework agnostic tool
tool, err := client.LoadTool("toolName", ctx)
if err != nil {
log.Fatalf("Failed to load tools: %v", err)
}
// Fetch the tool's input schema
inputschema, err := tool.InputSchema()
if err != nil {
log.Fatalf("Failed to fetch inputSchema: %v", err)
}
var paramsSchema openai.FunctionParameters
_ = json.Unmarshal(inputschema, &paramsSchema)
// Use this tool with OpenAI Go
openAITool := openai.ChatCompletionToolParam{
Function: openai.FunctionDefinitionParam{
Name: tool.Name(),
Description: openai.String(tool.Description()),
Parameters: paramsSchema,
},
}
}
{{< /highlight >}}
{{% /tab %}}
{{< /tabpane >}}
For more detailed instructions on using the Toolbox Go SDK, see the
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-go/blob/main/core/README.md).
For end-to-end samples on using the Toolbox Go SDK with orchestration frameworks, see the [project's samples](https://github.com/googleapis/mcp-toolbox-sdk-go/tree/main/core/samples)
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-js/blob/main/packages/toolbox-core/README.md).

View File

@@ -1,9 +1,9 @@
---
title: "Python Quickstart (Local)"
title: "Quickstart (Local)"
type: docs
weight: 2
description: >
How to get started running Toolbox locally with [Python](https://github.com/googleapis/mcp-toolbox-sdk-python), PostgreSQL, and [Agent Development Kit](https://google.github.io/adk-docs/),
How to get started running Toolbox locally with Python, PostgreSQL, and [Agent Development Kit](https://google.github.io/adk-docs/),
[LangGraph](https://www.langchain.com/langgraph), [LlamaIndex](https://www.llamaindex.ai/) or [GoogleGenAI](https://pypi.org/project/google-genai/).
---
@@ -14,21 +14,8 @@ description: >
This guide assumes you have already done the following:
1. Installed [Python 3.9+][install-python] (including [pip][install-pip] and
your preferred virtual environment tool for managing dependencies e.g. [venv][install-venv]).
1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres].
### Cloud Setup (Optional)
If you plan to use **Google Clouds Vertex AI** with your agent (e.g., using `vertexai=True` or a Google GenAI model), follow these one-time setup steps for local development:
1. [Install the Google Cloud CLI](https://cloud.google.com/sdk/docs/install)
1. [Set up Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)
1. Set your project and enable Vertex AI
```bash
gcloud config set project YOUR_PROJECT_ID
gcloud services enable aiplatform.googleapis.com
```
your preferred virtual environment tool for managing dependencies e.g. [venv][install-venv])
1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres]
[install-python]: https://wiki.python.org/moin/BeginnersGuide/Download
[install-pip]: https://pip.pypa.io/en/stable/installation/
@@ -154,7 +141,6 @@ postgres` and a password next time.
\q
```
## Step 2: Install and configure Toolbox
In this section, we will download Toolbox, configure our tools in a
@@ -170,7 +156,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.9.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->
@@ -646,7 +632,7 @@ asyncio.run(run_application())
{{< /tab >}}
{{< /tabpane >}}
{{< tabpane text=true persist=header >}}
{{< tabpane text=true persist=header >}}
{{% tab header="ADK" lang="en" %}}
To learn more about Agent Development Kit, check out the [ADK
documentation.](https://google.github.io/adk-docs/)
@@ -672,7 +658,3 @@ Documentation](https://github.com/googleapis/python-genai?tab=readme-ov-file#man
```sh
python hotel_agent.py
```
{{< notice info >}}
For more information, visit the [Python SDK repo](https://github.com/googleapis/mcp-toolbox-sdk-python).
{{</ notice >}}

View File

@@ -1,490 +0,0 @@
---
title: "JS Quickstart (Local)"
type: docs
weight: 3
description: >
How to get started running Toolbox locally with [JavaScript](https://github.com/googleapis/mcp-toolbox-sdk-python), PostgreSQL, and orchestration frameworks such as [LangChain](https://js.langchain.com/docs/introduction/) and [GenkitJS](https://genkit.dev/docs/get-started/).
---
## Before you begin
This guide assumes you have already done the following:
1. Installed [Node.js (v18 or higher)].
1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres].
### Cloud Setup (Optional)
If you plan to use **Google Clouds Vertex AI** with your agent (e.g., using Gemini or PaLM models), follow these one-time setup steps:
1. [Install the Google Cloud CLI]
1. [Set up Application Default Credentials (ADC)]
1. Set your project and enable Vertex AI
```bash
gcloud config set project YOUR_PROJECT_ID
gcloud services enable aiplatform.googleapis.com
```
[Node.js (v18 or higher)]: https://nodejs.org/
[install-postgres]: https://www.postgresql.org/download/
[Install the Google Cloud CLI]: https://cloud.google.com/sdk/docs/install
[Set up Application Default Credentials (ADC)]: https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment
## Step 1: Set up your database
In this section, we will create a database, insert some data that needs to be
accessed by our agent, and create a database user for Toolbox to connect with.
1. Connect to postgres using the `psql` command:
```bash
psql -h 127.0.0.1 -U postgres
```
Here, `postgres` denotes the default postgres superuser.
{{< notice info >}}
#### **Having trouble connecting?**
* **Password Prompt:** If you are prompted for a password for the `postgres`
user and do not know it (or a blank password doesn't work), your PostgreSQL
installation might require a password or a different authentication method.
* **`FATAL: role "postgres" does not exist`:** This error means the default
`postgres` superuser role isn't available under that name on your system.
* **`Connection refused`:** Ensure your PostgreSQL server is actually running.
You can typically check with `sudo systemctl status postgresql` and start it
with `sudo systemctl start postgresql` on Linux systems.
<br/>
#### **Common Solution**
For password issues or if the `postgres` role seems inaccessible directly, try
switching to the `postgres` operating system user first. This user often has
permission to connect without a password for local connections (this is called
peer authentication).
```bash
sudo -i -u postgres
psql -h 127.0.0.1
```
Once you are in the `psql` shell using this method, you can proceed with the
database creation steps below. Afterwards, type `\q` to exit `psql`, and then
`exit` to return to your normal user shell.
If desired, once connected to `psql` as the `postgres` OS user, you can set a
password for the `postgres` *database* user using: `ALTER USER postgres WITH
PASSWORD 'your_chosen_password';`. This would allow direct connection with `-U
postgres` and a password next time.
{{< /notice >}}
1. Create a new database and a new user:
{{< notice tip >}}
For a real application, it's best to follow the principle of least permission
and only grant the privileges your application needs.
{{< /notice >}}
```sql
CREATE USER toolbox_user WITH PASSWORD 'my-password';
CREATE DATABASE toolbox_db;
GRANT ALL PRIVILEGES ON DATABASE toolbox_db TO toolbox_user;
ALTER DATABASE toolbox_db OWNER TO toolbox_user;
```
1. End the database session:
```bash
\q
```
(If you used `sudo -i -u postgres` and then `psql`, remember you might also
need to type `exit` after `\q` to leave the `postgres` user's shell
session.)
1. Connect to your database with your new user:
```bash
psql -h 127.0.0.1 -U toolbox_user -d toolbox_db
```
1. Create a table using the following command:
```sql
CREATE TABLE hotels(
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL,
location VARCHAR NOT NULL,
price_tier VARCHAR NOT NULL,
checkin_date DATE NOT NULL,
checkout_date DATE NOT NULL,
booked BIT NOT NULL
);
```
1. Insert data into the table.
```sql
INSERT INTO hotels(id, name, location, price_tier, checkin_date, checkout_date, booked)
VALUES
(1, 'Hilton Basel', 'Basel', 'Luxury', '2024-04-22', '2024-04-20', B'0'),
(2, 'Marriott Zurich', 'Zurich', 'Upscale', '2024-04-14', '2024-04-21', B'0'),
(3, 'Hyatt Regency Basel', 'Basel', 'Upper Upscale', '2024-04-02', '2024-04-20', B'0'),
(4, 'Radisson Blu Lucerne', 'Lucerne', 'Midscale', '2024-04-24', '2024-04-05', B'0'),
(5, 'Best Western Bern', 'Bern', 'Upper Midscale', '2024-04-23', '2024-04-01', B'0'),
(6, 'InterContinental Geneva', 'Geneva', 'Luxury', '2024-04-23', '2024-04-28', B'0'),
(7, 'Sheraton Zurich', 'Zurich', 'Upper Upscale', '2024-04-27', '2024-04-02', B'0'),
(8, 'Holiday Inn Basel', 'Basel', 'Upper Midscale', '2024-04-24', '2024-04-09', B'0'),
(9, 'Courtyard Zurich', 'Zurich', 'Upscale', '2024-04-03', '2024-04-13', B'0'),
(10, 'Comfort Inn Bern', 'Bern', 'Midscale', '2024-04-04', '2024-04-16', B'0');
```
1. End the database session:
```bash
\q
```
## Step 2: Install and configure Toolbox
In this section, we will download Toolbox, configure our tools in a
`tools.yaml`, and then run the Toolbox server.
1. Download the latest version of Toolbox as a binary:
{{< notice tip >}}
Select the
[correct binary](https://github.com/googleapis/genai-toolbox/releases)
corresponding to your OS and CPU architecture.
{{< /notice >}}
<!-- {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.9.0/$OS/toolbox
```
<!-- {x-release-please-end} -->
1. Make the binary executable:
```bash
chmod +x toolbox
```
1. Write the following into a `tools.yaml` file. Be sure to update any fields
such as `user`, `password`, or `database` that you may have customized in the
previous step.
{{< notice tip >}}
In practice, use environment variable replacement with the format ${ENV_NAME}
instead of hardcoding your secrets into the configuration file.
{{< /notice >}}
```yaml
sources:
my-pg-source:
kind: postgres
host: 127.0.0.1
port: 5432
database: toolbox_db
user: ${USER_NAME}
password: ${PASSWORD}
tools:
search-hotels-by-name:
kind: postgres-sql
source: my-pg-source
description: Search for hotels based on name.
parameters:
- name: name
type: string
description: The name of the hotel.
statement: SELECT * FROM hotels WHERE name ILIKE '%' || $1 || '%';
search-hotels-by-location:
kind: postgres-sql
source: my-pg-source
description: Search for hotels based on location.
parameters:
- name: location
type: string
description: The location of the hotel.
statement: SELECT * FROM hotels WHERE location ILIKE '%' || $1 || '%';
book-hotel:
kind: postgres-sql
source: my-pg-source
description: >-
Book a hotel by its ID. If the hotel is successfully booked, returns a NULL, raises an error if not.
parameters:
- name: hotel_id
type: string
description: The ID of the hotel to book.
statement: UPDATE hotels SET booked = B'1' WHERE id = $1;
update-hotel:
kind: postgres-sql
source: my-pg-source
description: >-
Update a hotel's check-in and check-out dates by its ID. Returns a message
indicating whether the hotel was successfully updated or not.
parameters:
- name: hotel_id
type: string
description: The ID of the hotel to update.
- name: checkin_date
type: string
description: The new check-in date of the hotel.
- name: checkout_date
type: string
description: The new check-out date of the hotel.
statement: >-
UPDATE hotels SET checkin_date = CAST($2 as date), checkout_date = CAST($3
as date) WHERE id = $1;
cancel-hotel:
kind: postgres-sql
source: my-pg-source
description: Cancel a hotel by its ID.
parameters:
- name: hotel_id
type: string
description: The ID of the hotel to cancel.
statement: UPDATE hotels SET booked = B'0' WHERE id = $1;
toolsets:
my-toolset:
- search-hotels-by-name
- search-hotels-by-location
- book-hotel
- update-hotel
- cancel-hotel
```
For more info on tools, check out the `Resources` section of the docs.
1. Run the Toolbox server, pointing to the `tools.yaml` file created earlier:
```bash
./toolbox --tools-file "tools.yaml"
```
{{< notice note >}}
Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
{{< /notice >}}
## Step 3: Connect your agent to Toolbox
In this section, we will write and run an agent that will load the Tools
from Toolbox.
1. (Optional) Initialize a Node.js project:
```bash
npm init -y
```
1. In a new terminal, install the [SDK](https://www.npmjs.com/package/@toolbox-sdk/core).
```bash
npm install langchain @toolbox-sdk/core
```
1. Install other required dependencies
{{< tabpane persist=header >}}
{{< tab header="LangChain" lang="bash" >}}
npm install langchain @langchain/google-vertexai
{{< /tab >}}
{{< tab header="GenkitJS" lang="bash" >}}
npm install genkit @genkit-ai/vertexai
{{< /tab >}}
{{< /tabpane >}}
1. Create a new file named `hotelAgent.js` and copy the following code to create an agent:
{{< tabpane persist=header >}}
{{< tab header="LangChain" lang="js" >}}
import { ChatVertexAI } from "@langchain/google-vertexai";
import { ToolboxClient } from "@toolbox-sdk/core";
import { tool } from "@langchain/core/tools";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { MemorySaver } from "@langchain/langgraph";
// Replace it with your API key
process.env.GOOGLE_API_KEY = 'your-api-key';
const prompt = `
You're a helpful hotel assistant. You handle hotel searching, booking, and
cancellations. When the user searches for a hotel, mention its name, id,
location and price tier. Always mention hotel ids while performing any
searches. This is very important for any operations. For any bookings or
cancellations, please provide the appropriate confirmation. Be sure to
update checkin or checkout dates if mentioned by the user.
Don't ask for confirmations from the user.
`;
const queries = [
"Find hotels in Basel with Basel in its name.",
"Can you book the Hilton Basel for me?",
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
"My check in dates would be from April 10, 2024 to April 19, 2024.",
];
async function runApplication() {
const model = new ChatVertexAI({
model: "gemini-2.0-flash",
});
const client = new ToolboxClient("http://127.0.0.1:5000");
const toolboxTools = await client.loadToolset("my-toolset");
// Define the basics of the tool: name, description, schema and core logic
const getTool = (toolboxTool) => tool(toolboxTool, {
name: toolboxTool.getName(),
description: toolboxTool.getDescription(),
schema: toolboxTool.getParamSchema()
});
const tools = toolboxTools.map(getTool);
const agent = createReactAgent({
llm: model,
tools: tools,
checkpointer: new MemorySaver(),
systemPrompt: prompt,
});
const langGraphConfig = {
configurable: {
thread_id: "test-thread",
},
};
for (const query of queries) {
const agentOutput = await agent.invoke(
{
messages: [
{
role: "user",
content: query,
},
],
verbose: true,
},
langGraphConfig
);
const response = agentOutput.messages[agentOutput.messages.length - 1].content;
console.log(response);
}
}
runApplication()
.catch(console.error)
.finally(() => console.log("\nApplication finished."));
{{< /tab >}}
{{< tab header="GenkitJS" lang="js" >}}
import { ToolboxClient } from "@toolbox-sdk/core";
import { genkit } from "genkit";
import { googleAI } from '@genkit-ai/googleai';
// Replace it with your API key
process.env.GOOGLE_API_KEY = 'your-api-key';
const systemPrompt = `
You're a helpful hotel assistant. You handle hotel searching, booking, and
cancellations. When the user searches for a hotel, mention its name, id,
location and price tier. Always mention hotel ids while performing any
searches. This is very important for any operations. For any bookings or
cancellations, please provide the appropriate confirmation. Be sure to
update checkin or checkout dates if mentioned by the user.
Don't ask for confirmations from the user.
`;
const queries = [
"Find hotels in Basel with Basel in its name.",
"Can you book the Hilton Basel for me?",
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
"My check in dates would be from April 10, 2024 to April 19, 2024.",
];
async function run() {
const toolboxClient = new ToolboxClient("http://127.0.0.1:5000");
const ai = genkit({
plugins: [
googleAI({
apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
})
],
model: googleAI.model('gemini-2.0-flash'),
});
const toolboxTools = await toolboxClient.loadToolset("my-toolset");
const toolMap = Object.fromEntries(
toolboxTools.map((tool) => {
const definedTool = ai.defineTool(
{
name: tool.getName(),
description: tool.getDescription(),
inputSchema: tool.getParamSchema(),
},
tool
);
return [tool.getName(), definedTool];
})
);
const tools = Object.values(toolMap);
let conversationHistory = [{ role: "system", content: [{ text: systemPrompt }] }];
for (const query of queries) {
conversationHistory.push({ role: "user", content: [{ text: query }] });
const response = await ai.generate({
messages: conversationHistory,
tools: tools,
});
conversationHistory.push(response.message);
const toolRequests = response.toolRequests;
if (toolRequests?.length > 0) {
// Execute tools concurrently and collect their responses.
const toolResponses = await Promise.all(
toolRequests.map(async (call) => {
try {
const toolOutput = await toolMap[call.name].invoke(call.input);
return { role: "tool", content: [{ toolResponse: { name: call.name, output: toolOutput } }] };
} catch (e) {
console.error(`Error executing tool ${call.name}:`, e);
return { role: "tool", content: [{ toolResponse: { name: call.name, output: { error: e.message } } }] };
}
})
);
conversationHistory.push(...toolResponses);
// Call the AI again with the tool results.
response = await ai.generate({ messages: conversationHistory, tools });
conversationHistory.push(response.message);
}
console.log(response.text);
}
}
run();
{{< /tab >}}
{{< /tabpane >}}
1. Run your agent, and observe the results:
```sh
node hotelAgent.js
```
{{< notice info >}}
For more information, visit the [JS SDK repo](https://github.com/googleapis/mcp-toolbox-sdk-js).
{{</ notice >}}

View File

@@ -105,7 +105,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.9.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->
@@ -218,25 +218,17 @@ In this section, we will download Toolbox, configure our tools in a
1. Type `y` when it asks to install the inspector package.
1. It should show the following when the MCP Inspector is up and running (please take note of `<YOUR_SESSION_TOKEN>`):
1. It should show the following when the MCP Inspector is up and running:
```bash
Starting MCP inspector...
⚙️ Proxy server listening on localhost:6277
🔑 Session token: <YOUR_SESSION_TOKEN>
Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth
🚀 MCP Inspector is up and running at:
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<YOUR_SESSION_TOKEN>
🔍 MCP Inspector is up and running at http://127.0.0.1:5173 🚀
```
1. Open the above link in your browser.
1. For `Transport Type`, select `Streamable HTTP`.
1. For `Transport Type`, select `SSE`.
1. For `URL`, type in `http://127.0.0.1:5000/mcp`.
1. For `Configuration` -> `Proxy Session Token`, make sure `<YOUR_SESSION_TOKEN>` is present.
1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse`.
1. Click Connect.
@@ -246,4 +238,4 @@ In this section, we will download Toolbox, configure our tools in a
![inspector_tools](./inspector_tools.png)
1. Test out your tools here!
1. Test out your tools here!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -52,19 +52,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.9.0/linux/amd64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/linux/amd64/toolbox>
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.9.0/darwin/arm64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/darwin/arm64/toolbox>
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.9.0/darwin/amd64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/darwin/amd64/toolbox>
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.9.0/windows/amd64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/windows/amd64/toolbox>
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -20,16 +20,18 @@ The native SDKs can be combined with MCP clients in many cases.
Toolbox currently supports the following versions of MCP specification:
* [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18)
* [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26)
* [2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05)
* [2024-11-05](https://spec.modelcontextprotocol.io/specification/2024-11-05/)
### Toolbox AuthZ/AuthN Not Supported by MCP
### Features Not Supported by MCP
The auth implementation in Toolbox is not supported in MCP's auth specification.
This includes:
Toolbox has several features that are not yet supported in the MCP specification:
* **AuthZ/AuthN:** There are no auth implementation in the `2024-11-05`
specification. This includes:
* [Authenticated Parameters](../resources/tools/_index.md#authenticated-parameters)
* [Authorized Invocations](../resources/tools/_index.md#authorized-invocations)
* **Notifications:** Currently, editing Toolbox Tools requires a server restart.
Clients should reload tools on disconnect to get the latest version.
## Connecting to Toolbox with an MCP client
@@ -69,7 +71,7 @@ Toolbox enables dynamic reloading by default. To disable, use the `--disable-rel
Toolbox supports the HTTP transport protocol with and without SSE.
{{< tabpane text=true >}} {{% tab header="HTTP with SSE (deprecated)" lang="en" %}}
{{< tabpane text=true >}} {{% tab header="HTTP with SSE" lang="en" %}}
Add the following configuration to your MCP client configuration:
```bash
@@ -85,25 +87,11 @@ Add the following configuration to your MCP client configuration:
If you would like to connect to a specific toolset, replace `url` with
`"http://127.0.0.1:5000/mcp/{toolset_name}/sse"`.
{{% /tab %}} {{% tab header="HTTP POST" lang="en" %}}
Connect to Toolbox HTTP POST via `http://127.0.0.1:5000/mcp`.
HTTP with SSE is only supported in version `2024-11-05` and is currently
deprecated.
{{% /tab %}} {{% tab header="Streamable HTTP" lang="en" %}}
Add the following configuration to your MCP client configuration:
```bash
{
"mcpServers": {
"toolbox": {
"type": "http",
"url": "http://127.0.0.1:5000/mcp",
}
}
}
```
If you would like to connect to a specific toolset, replace `url` with
`"http://127.0.0.1:5000/mcp/{toolset_name}"`.
If you would like to connect to a specific toolset, connect via
`http://127.0.0.1:5000/mcp/{toolset_name}`.
{{% /tab %}} {{< /tabpane >}}
### Using the MCP Inspector with Toolbox
@@ -130,7 +118,7 @@ testing and debugging Toolbox server.
1. Click the `Connect` button. It might take awhile to spin up Toolbox. Voila!
You should be able to inspect your toolbox tools!
{{% /tab %}}
{{% tab header="HTTP with SSE (deprecated)" lang="en" %}}
{{% tab header="HTTP with SSE" lang="en" %}}
1. [Run Toolbox](../getting-started/introduction/_index.md#running-the-server).
1. In a separate terminal, run Inspector directly through `npx`:
@@ -144,23 +132,6 @@ testing and debugging Toolbox server.
1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse` to use all tool or
`http//127.0.0.1:5000/mcp/{toolset_name}/sse` to use a specific toolset.
1. Click the `Connect` button. Voila! You should be able to inspect your toolbox
tools!
{{% /tab %}}
{{% tab header="Streamable HTTP" lang="en" %}}
1. [Run Toolbox](../getting-started/introduction/_index.md#running-the-server).
1. In a separate terminal, run Inspector directly through `npx`:
```bash
npx @modelcontextprotocol/inspector
```
1. For `Transport Type` dropdown menu, select `Streamable HTTP`.
1. For `URL`, type in `http://127.0.0.1:5000/mcp` to use all tool or
`http//127.0.0.1:5000/mcp/{toolset_name}` to use a specific toolset.
1. Click the `Connect` button. Voila! You should be able to inspect your toolbox
tools!
{{% /tab %}} {{< /tabpane >}}

View File

@@ -4,6 +4,7 @@ type: docs
weight: 1
description: >
MySQL is a relational database management system that stores and manages data.
---
## About
@@ -34,7 +35,6 @@ sources:
database: my_db
user: ${USER_NAME}
password: ${PASSWORD}
queryTimeout: 30s # Optional: query timeout duration
```
{{< notice tip >}}
@@ -44,12 +44,11 @@ instead of hardcoding your secrets into the configuration file.
## Reference
| **field** | **type** | **required** | **description** |
| ------------ | :------: | :----------: | ----------------------------------------------------------------------------------------------- |
| kind | string | true | Must be "mysql". |
| host | string | true | IP address to connect to (e.g. "127.0.0.1"). |
| port | string | true | Port to connect to (e.g. "3306"). |
| database | string | true | Name of the MySQL database to connect to (e.g. "my_db"). |
| user | string | true | Name of the MySQL user to connect as (e.g. "my-mysql-user"). |
| password | string | true | Password of the MySQL user (e.g. "my-password"). |
| queryTimeout | string | false | Maximum time to wait for query execution (e.g. "30s", "2m"). By default, no timeout is applied. |
| **field** | **type** | **required** | **description** |
|-----------|:--------:|:------------:|---------------------------------------------------------------------------------------------|
| kind | string | true | Must be "mysql". |
| host | string | true | IP address to connect to (e.g. "127.0.0.1"). |
| port | string | true | Port to connect to (e.g. "3306"). |
| database | string | true | Name of the MySQL database to connect to (e.g. "my_db"). |
| user | string | true | Name of the MySQL user to connect as (e.g. "my-mysql-user"). |
| password | string | true | Password of the MySQL user (e.g. "my-password"). |

View File

@@ -33,7 +33,7 @@ sources:
my-redis-instance:
kind: redis
address:
- 127.0.0.1:6379
- 127.0.0.1
username: ${MY_USER_NAME}
password: ${MY_AUTH_STRING} # Omit this field if you don't have a password.
# database: 0
@@ -58,7 +58,7 @@ sources:
my-redis-cluster-instance:
kind: memorystore-redis
address:
- 127.0.0.1:6379
- 127.0.0.1
password: ${MY_AUTH_STRING}
# useGCPIAM: false
# clusterEnabled: false
@@ -74,8 +74,7 @@ using IAM authentication:
sources:
my-redis-cluster-instance:
kind: memorystore-redis
address:
- 127.0.0.1:6379
address: 127.0.0.1
useGCPIAM: true
clusterEnabled: true
```

View File

@@ -17,7 +17,7 @@ sets, sorted sets with range queries, bitmaps, hyperloglogs, and geospatial
indexes with radius queries.
If you're new to Valkey, you can find installation and getting started guides on
the [official Valkey website](https://valkey.io/topics/quickstart/).
the [official Valkey website](https://valkey.io/docs/getting-started/).
## Example
@@ -26,7 +26,7 @@ sources:
my-valkey-instance:
kind: valkey
address:
- 127.0.0.1:6379
- 127.0.0.1
username: ${YOUR_USERNAME}
password: ${YOUR_PASSWORD}
# database: 0
@@ -50,7 +50,7 @@ sources:
my-valkey-instance:
kind: valkey
address:
- 127.0.0.1:6379
- 127.0.0.1
useGCPIAM: true
```

View File

@@ -77,12 +77,12 @@ the parameter.
description: Airline unique 2 letter identifier
```
| **field** | **type** | **required** | **description** |
|-------------|:--------------:|:------------:|-----------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be one of "string", "integer", "float", "boolean" "array" |
| default | parameter type | false | Default value of the parameter. If provided, the parameter is not required. |
| description | string | true | Natural language description of the parameter to describe it to the agent. |
| **field** | **type** | **required** | **description** |
|-------------|:---------------:|:------------:|-----------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be one of "string", "integer", "float", "boolean" "array" |
| default | parameter type | false | Default value of the parameter. If provided, the parameter is not required. |
| description | string | true | Natural language description of the parameter to describe it to the agent. |
### Array Parameters
@@ -107,7 +107,7 @@ in the list using the items field:
|-------------|:----------------:|:------------:|-----------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be "array" |
| default | parameter type | false | Default value of the parameter. If provided, the parameter is not required. |
| default | parameter type | false | Default value of the parameter. If provided, the parameter is not required. |
| description | string | true | Natural language description of the parameter to describe it to the agent. |
| items | parameter object | true | Specify a Parameter object for the type of the values in the array. |
@@ -115,43 +115,6 @@ in the list using the items field:
Items in array should not have a default value. If provided, it will be ignored.
{{< /notice >}}
### Object Parameters
The object type is a collection of key-value pairs passed in as a single
parameter. To use the object type, you must specify the schema for each
key-value pair using the properties field.
```yaml
parameters:
- name: new_user
type: object
description: A new user's profile information.
properties:
name:
type: string
description: The full name of the user.
age:
type: integer
description: The age of the user.
is_subscriber:
type: boolean
description: Whether the user is a subscriber.
statement: |
INSERT INTO users (name, age, is_subscriber) VALUES ($1->>'name', ($1->>'age')::integer, ($1->>'is_subscriber')::boolean);
```
| **field** | **type** | **required** | **description** |
|-------------|:------------------------:|:------------:|---------------------------------------------------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be "object" |
| default | parameter type | false | Default value of the parameter. If provided, the parameter is not required. |
| description | string | true | Natural language description of the parameter to describe it to the agent. |
| properties | map of parameter objects | true | A map where each key is a property name and each value is a Parameter object defining the schema for that property. |
{{< notice note >}}
Properties within an object should not have a default value. If provided, it will be ignored. A default can only be provided for the top-level object parameter.
{{< /notice >}}
### Authenticated Parameters
Authenticated parameters are automatically populated with user
@@ -180,10 +143,10 @@ user's ID token.
field: sub
```
| **field** | **type** | **required** | **description** |
|-----------|:--------:|:------------:|---------------------------------------------------------------------------------|
| **field** | **type** | **required** | **description** |
|-----------|:--------:|:------------:|-----------------------------------------------------------------------------------------|
| name | string | true | Name of the [authServices](../authservices) used to verify the OIDC auth token. |
| field | string | true | Claim field decoded from the OIDC token used to auto-populate this parameter. |
| field | string | true | Claim field decoded from the OIDC token used to auto-populate this parameter. |
### Template Parameters
@@ -232,12 +195,12 @@ tools:
description: Name of a column to select
```
| **field** | **type** | **required** | **description** |
|-------------|:----------------:|:---------------:|-------------------------------------------------------------------------------------|
| name | string | true | Name of the template parameter. |
| type | string | true | Must be one of "string", "integer", "float", "boolean" "array" |
| description | string | true | Natural language description of the template parameter to describe it to the agent. |
| items | parameter object | true (if array) | Specify a Parameter object for the type of the values in the array (string only). |
| **field** | **type** | **required** | **description** |
|-------------|:----------------:|:-------------:|-------------------------------------------------------------------------------------|
| name | string | true | Name of the template parameter. |
| type | string | true | Must be one of "string", "integer", "float", "boolean" "array" |
| description | string | true | Natural language description of the template parameter to describe it to the agent. |
| items | parameter object |true (if array)| Specify a Parameter object for the type of the values in the array (string only). |
## Authorized Invocations

View File

@@ -20,11 +20,8 @@ instance. It's compatible with any of the following sources:
Bigtable supports SQL queries. The integration with Toolbox supports `googlesql`
dialect, the specified SQL statement is executed as a [data manipulation
language (DML)][bigtable-googlesql] statements, and specified parameters will inserted according to their name: e.g. `@name`.
{{<notice note>}}
Bigtable's GoogleSQL support for DML statements might be limited to certain query types. For detailed information on supported DML statements and use cases, refer to the [Bigtable GoogleSQL use cases](https://cloud.google.com/bigtable/docs/googlesql-overview#use-cases).
{{</notice>}}
language (DML)][bigtable-googlesql] statements, and specified parameters will
inserted according to their name: e.g. `@name`.
[bigtable-googlesql]: https://cloud.google.com/bigtable/docs/googlesql-overview

View File

@@ -1,37 +0,0 @@
---
title: "wait"
type: docs
weight: 1
description: >
A "wait" tool pauses execution for a specified duration.
aliases:
- /resources/tools/utility/wait
---
## About
A `wait` tool pauses execution for a specified duration. This can be useful in workflows where a delay is needed between steps.
`wait` takes one input parameter `duration` which is a string representing the time to wait (e.g., "10s", "2m", "1h").
{{% notice info %}}
This tool is intended for developer assistant workflows with human-in-the-loop and shouldn't be used for production agents.
{{% /notice %}}
## Example
```yaml
tools:
wait_for_tool:
kind: wait
description: Use this tool to pause execution for a specified duration.
timeout: 30s
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "wait". |
| description | string | true | Description of the tool that is passed to the LLM. |
| timeout | string | true | The default duration the tool can wait for. |

View File

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

View File

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

View File

@@ -98,7 +98,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.9.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->
@@ -208,25 +208,17 @@ In this section, we will download Toolbox, configure our tools in a
1. Type `y` when it asks to install the inspector package.
1. It should show the following when the MCP Inspector is up and running (please take note of `<YOUR_SESSION_TOKEN>`):
1. It should show the following when the MCP Inspector is up and running:
```bash
Starting MCP inspector...
⚙️ Proxy server listening on localhost:6277
🔑 Session token: <YOUR_SESSION_TOKEN>
Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth
🚀 MCP Inspector is up and running at:
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<YOUR_SESSION_TOKEN>
🔍 MCP Inspector is up and running at http://127.0.0.1:5173 🚀
```
1. Open the above link in your browser.
1. For `Transport Type`, select `Streamable HTTP`.
1. For `Transport Type`, select `SSE`.
1. For `URL`, type in `http://127.0.0.1:5000/mcp`.
1. For `Configuration` -> `Proxy Session Token`, make sure `<YOUR_SESSION_TOKEN>` is present.
1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse`.
1. Click Connect.
@@ -236,4 +228,4 @@ In this section, we will download Toolbox, configure our tools in a
![inspector_tools](./inspector_tools.png)
1. Test out your tools here!
1. Test out your tools here!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,7 +0,0 @@
---
title: "SDKs"
type: docs
weight: 6
description: >
Client SDKs to connect to the MCP Toolbox server.
---

View File

@@ -1,15 +0,0 @@
---
title: "Go SDK"
weight: 2
description: Go lang client SDK
icon: fa-brands fa-golang
manualLink: "https://github.com/googleapis/mcp-toolbox-sdk-go"
manualLinkTarget: _blank
---
<html>
<head>
<link rel="canonical" href="https://github.com/googleapis/mcp-toolbox-sdk-go"/>
<meta http-equiv="refresh" content="0;url=https://github.com/googleapis/mcp-toolbox-sdk-go"/>
</head>
</html>

View File

@@ -1,15 +0,0 @@
---
title: "JS SDK"
weight: 2
description: Javascript client SDK
icon: fa-brands fa-node-js
manualLink: "https://github.com/googleapis/mcp-toolbox-sdk-js"
manualLinkTarget: _blank
---
<html>
<head>
<link rel="canonical" href="https://github.com/googleapis/mcp-toolbox-sdk-js"/>
<meta http-equiv="refresh" content="0;url=https://github.com/googleapis/mcp-toolbox-sdk-js"/>
</head>
</html>

View File

@@ -1,15 +0,0 @@
---
title: "Python SDK"
weight: 2
description: Python client SDK
icon: fa-brands fa-python
manualLink: "https://github.com/googleapis/mcp-toolbox-sdk-python"
manualLinkTarget: _blank
---
<html>
<head>
<link rel="canonical" href="https://github.com/googleapis/mcp-toolbox-sdk-python"/>
<meta http-equiv="refresh" content="0;url=hhttps://github.com/googleapis/mcp-toolbox-sdk-python"/>
</head>
</html>

59
go.mod
View File

@@ -2,13 +2,13 @@ module github.com/googleapis/genai-toolbox
go 1.23.8
toolchain go1.24.5
toolchain go1.24.4
require (
cloud.google.com/go/alloydbconn v1.15.4
cloud.google.com/go/alloydbconn v1.15.3
cloud.google.com/go/bigquery v1.69.0
cloud.google.com/go/bigtable v1.38.0
cloud.google.com/go/cloudsqlconn v1.17.3
cloud.google.com/go/cloudsqlconn v1.17.2
cloud.google.com/go/spanner v1.83.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0
@@ -18,6 +18,7 @@ require (
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/httplog/v2 v2.1.1
github.com/go-chi/render v1.0.3
github.com/go-goquery/goquery v1.0.1
github.com/go-playground/validator/v10 v10.27.0
github.com/go-sql-driver/mysql v1.9.3
github.com/goccy/go-yaml v1.18.0
@@ -29,17 +30,17 @@ require (
github.com/neo4j/neo4j-go-driver/v5 v5.28.1
github.com/redis/go-redis/v9 v9.11.0
github.com/spf13/cobra v1.9.1
github.com/valkey-io/valkey-go v1.0.63
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0
go.opentelemetry.io/otel/metric v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
github.com/valkey-io/valkey-go v1.0.62
go.opentelemetry.io/contrib/propagators/autoprop v0.61.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0
go.opentelemetry.io/otel/metric v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/sdk/metric v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/oauth2 v0.30.0
google.golang.org/api v0.242.0
google.golang.org/api v0.240.0
modernc.org/sqlite v1.38.0
)
@@ -48,7 +49,7 @@ require golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
require (
cel.dev/expr v0.23.0 // indirect
cloud.google.com/go v0.121.2 // indirect
cloud.google.com/go/alloydb v1.18.0 // indirect
cloud.google.com/go/alloydb v1.16.1 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
@@ -60,7 +61,9 @@ require (
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/apache/arrow/go/v15 v15.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -92,7 +95,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -117,26 +120,26 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.36.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
modernc.org/libc v1.65.10 // indirect

148
go.sum
View File

@@ -53,10 +53,10 @@ cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9j
cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ=
cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k=
cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw=
cloud.google.com/go/alloydb v1.18.0 h1:P+s1oek+sF3MlcumZuOj2ueUlusVwr3IT0R0vUbMA88=
cloud.google.com/go/alloydb v1.18.0/go.mod h1:iB/PmQYLHwDXCGCc0weeL5ORP6GadFjXJlRZ9pE0vSY=
cloud.google.com/go/alloydbconn v1.15.4 h1:RvtwKVq0YxYQFTKaW5jQWGPAVSvxO3ebqKj2oyl009A=
cloud.google.com/go/alloydbconn v1.15.4/go.mod h1:m5Db60PJv75Hz9uIaIIJR7ZPQazVC4VGxlhxNqYCBjk=
cloud.google.com/go/alloydb v1.16.1 h1:pW4D0O2jAfAjoOEI1bgChPwMHWE8X8BjwSO0tfWkWvk=
cloud.google.com/go/alloydb v1.16.1/go.mod h1:zeZuGJ5mEaQE70FMXEvZIp5hQLR9yrGnHo1YUOncWRY=
cloud.google.com/go/alloydbconn v1.15.3 h1:j0Y0+LpVjdyUguX0uwsaeTtq4tQUZiFvsO52AH+yusY=
cloud.google.com/go/alloydbconn v1.15.3/go.mod h1:9yrNzUeMr3wR/D4gTJrh5ph2VDW/19tAMV7TlNuyRfM=
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M=
@@ -167,8 +167,8 @@ cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb
cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM=
cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk=
cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA=
cloud.google.com/go/cloudsqlconn v1.17.3 h1:dAEgQmhj9NHVRqven4elTBCbOWOtFPSNjAqBoznJSpc=
cloud.google.com/go/cloudsqlconn v1.17.3/go.mod h1:5AHAXT4hbs2+EbzNDBxPu9QU+tJwRZyWNPwwiE8MzRs=
cloud.google.com/go/cloudsqlconn v1.17.2 h1:SxSt6ujMxK1KyxKAI2Z5raT2n3geN7ipu6bA8f7iR7E=
cloud.google.com/go/cloudsqlconn v1.17.2/go.mod h1:l7NymuoD+hycOo+92SJEyETPtE05oRG4oXjcH3swftw=
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4=
@@ -661,6 +661,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
@@ -668,6 +670,8 @@ github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
@@ -784,6 +788,8 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-goquery/goquery v1.0.1 h1:kpchVA1LdOFWdRpkDPESVdlb1JQI6ixsJ5MiNUITO7U=
github.com/go-goquery/goquery v1.0.1/go.mod h1:W5s8OWbqWf6lG0LkXWBeh7U1Y/X5XTI0Br65MHF8uJk=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -885,6 +891,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -949,8 +956,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vb
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
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=
@@ -1094,8 +1101,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valkey-io/valkey-go v1.0.63 h1:LNlDTcUxy9jxrmGHSvd0s/NsgEmQbvREYvvBAHCIir0=
github.com/valkey-io/valkey-go v1.0.63/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/valkey-io/valkey-go v1.0.62 h1:oQdPlQGRyxcQWL8fnu6J3SCaQwayc/hRZifjJIaJqu0=
github.com/valkey-io/valkey-go v1.0.62/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1128,37 +1135,37 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI=
go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss=
go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E=
go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 h1:9PgnL3QNlj10uGxExowIDIZu66aVBwWhXmbOp1pa6RA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0/go.mod h1:0ineDcLELf6JmKfuo0wvvhAVMuxWFYvkTin2iV4ydPQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/contrib/propagators/autoprop v0.61.0 h1:cxOVDJ30qfzV27G5p9WMtJUB/3cXC0iL+u9EV1fSOws=
go.opentelemetry.io/contrib/propagators/autoprop v0.61.0/go.mod h1:Y+xiUbWetg65vAroDZcIzJ5wyPNWRH32EoIV9rIaa0g=
go.opentelemetry.io/contrib/propagators/aws v1.36.0 h1:Txhy/1LZIbbnutftc5pdU8Y9vOQuAkuIOFXuLsdDejs=
go.opentelemetry.io/contrib/propagators/aws v1.36.0/go.mod h1:M3A0491jGFPNHU8b3zEW7r/gtsMpGOsFUO3WL+SZ1xw=
go.opentelemetry.io/contrib/propagators/b3 v1.36.0 h1:xrAb/G80z/l5JL6XlmUMSD1i6W8vXkWrLfmkD3w/zZo=
go.opentelemetry.io/contrib/propagators/b3 v1.36.0/go.mod h1:UREJtqioFu5awNaCR8aEx7MfJROFlAWb6lPaJFbHaG0=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM=
go.opentelemetry.io/contrib/propagators/ot v1.36.0 h1:UBoZjbx483GslNKYK2YpfvePTJV4BHGeFd8+b7dexiM=
go.opentelemetry.io/contrib/propagators/ot v1.36.0/go.mod h1:adDDRry19/n9WoA7mSCMjoVJcmzK/bZYzX9SR+g2+W4=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -1178,8 +1185,12 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1239,6 +1250,9 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1298,8 +1312,13 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1347,8 +1366,12 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1428,8 +1451,14 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -1438,6 +1467,11 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1454,8 +1488,12 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1528,8 +1566,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1607,8 +1647,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.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU=
google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
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=
@@ -1753,8 +1793,8 @@ google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRx
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
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=

View File

@@ -133,7 +133,6 @@ func toolGetHandler(s *Server, w http.ResponseWriter, r *http.Request) {
func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
ctx, span := s.instrumentation.Tracer.Start(r.Context(), "toolbox/server/tool/invoke")
r = r.WithContext(ctx)
ctx = util.WithLogger(r.Context(), s.logger)
toolName := chi.URLParam(r, "toolName")
s.logger.DebugContext(ctx, fmt.Sprintf("tool name: %s", toolName))

View File

@@ -55,6 +55,8 @@ type ServerConfig struct {
Stdio bool
// DisableReload indicates if the user has disabled dynamic reloading for Toolbox.
DisableReload bool
// UI indicates if Toolbox UI endpoints (/ui) are available
UI bool
}
type logFormat string

View File

@@ -357,17 +357,6 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
protocolVersion = v20250326.PROTOCOL_VERSION
}
// check if client have `MCP-Protocol-Version` header
headerProtocolVersion := r.Header.Get("MCP-Protocol-Version")
if headerProtocolVersion != "" {
if !mcp.VerifyProtocolVersion(headerProtocolVersion) {
err := fmt.Errorf("invalid protocol version: %s", headerProtocolVersion)
_ = render.Render(w, r, newErrResponse(err, http.StatusBadRequest))
return
}
protocolVersion = headerProtocolVersion
}
toolsetName := chi.URLParam(r, "toolsetName")
s.logger.DebugContext(ctx, fmt.Sprintf("toolset name: %s", toolsetName))
span.SetAttributes(attribute.String("toolset_name", toolsetName))
@@ -398,7 +387,6 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
id := uuid.New().String()
s.logger.DebugContext(ctx, err.Error())
render.JSON(w, r, jsonrpc.NewError(id, jsonrpc.PARSE_ERROR, err.Error(), nil))
return
}
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName)
@@ -443,6 +431,10 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
if protocolVersion == "" {
protocolVersion = v20241105.PROTOCOL_VERSION
}
// Generic baseMessage could either be a JSONRPCNotification or JSONRPCRequest
var baseMessage jsonrpc.BaseMessage
if err = util.DecodeJSON(bytes.NewBuffer(body), &baseMessage); err != nil {

View File

@@ -24,20 +24,15 @@ import (
mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
v20250326 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250326"
v20250618 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250618"
"github.com/googleapis/genai-toolbox/internal/tools"
)
// LATEST_PROTOCOL_VERSION is the latest version of the MCP protocol supported.
// Update the version used in InitializeResponse when this value is updated.
const LATEST_PROTOCOL_VERSION = v20250618.PROTOCOL_VERSION
const LATEST_PROTOCOL_VERSION = v20250326.PROTOCOL_VERSION
// SUPPORTED_PROTOCOL_VERSIONS is the MCP protocol versions that are supported.
var SUPPORTED_PROTOCOL_VERSIONS = []string{
v20241105.PROTOCOL_VERSION,
v20250326.PROTOCOL_VERSION,
v20250618.PROTOCOL_VERSION,
}
var SUPPORTED_PROTOCOL_VERSIONS = []string{v20241105.PROTOCOL_VERSION, v20250326.PROTOCOL_VERSION}
// InitializeResponse runs capability negotiation and protocol version agreement.
// This is the Initialization phase of the lifecycle for MCP client-server connections.
@@ -66,9 +61,7 @@ func InitializeResponse(ctx context.Context, id jsonrpc.RequestId, body []byte,
},
},
ServerInfo: mcputil.Implementation{
BaseMetadata: mcputil.BaseMetadata{
Name: mcputil.SERVER_NAME,
},
Name: mcputil.SERVER_NAME,
Version: toolboxVersion,
},
}
@@ -95,16 +88,12 @@ func NotificationHandler(ctx context.Context, body []byte) error {
// This is the Operation phase of the lifecycle for MCP client-server connections.
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte) (any, error) {
switch mcpVersion {
case v20250618.PROTOCOL_VERSION:
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, body)
case v20250326.PROTOCOL_VERSION:
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, body)
default:
case v20241105.PROTOCOL_VERSION:
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, body)
default:
err := fmt.Errorf("invalid protocol version: %s", mcpVersion)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
}
// VerifyProtocolVersion verifies if the version string is valid.
func VerifyProtocolVersion(version string) bool {
return slices.Contains(SUPPORTED_PROTOCOL_VERSIONS, version)
}

View File

@@ -89,22 +89,8 @@ type ServerCapabilities struct {
Tools *ListChanged `json:"tools,omitempty"`
}
// Base interface for metadata with name (identifier) and title (display name) properties.
type BaseMetadata struct {
// Intended for programmatic or logical use, but used as a display name in past specs
// or fallback (if title isn't present).
Name string `json:"name"`
// Intended for UI and end-user contexts — optimized to be human-readable and easily understood,
//even by those unfamiliar with domain-specific terminology.
//
// If not provided, the name should be used for display (except for Tool,
// where `annotations.title` should be given precedence over using `name`,
// if present).
Title string `json:"title,omitempty"`
}
// Implementation describes the name and version of an MCP implementation.
type Implementation struct {
BaseMetadata
Name string `json:"name"`
Version string `json:"version"`
}

View File

@@ -1,141 +0,0 @@
// 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 v20250618
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
)
// ProcessMethod returns a response for the request.
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte) (any, error) {
switch method {
case TOOLS_LIST:
return toolsListHandler(id, toolset, body)
case TOOLS_CALL:
return toolsCallHandler(ctx, id, tools, body)
default:
err := fmt.Errorf("invalid method %s", method)
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
}
}
func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte) (any, error) {
var req ListToolsRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp tools list request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
result := ListToolsResult{
Tools: toolset.McpManifest,
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}
// toolsCallHandler generate a response for tools call.
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
var req CallToolRequest
if err = json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp tools call request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
toolName := req.Params.Name
toolArgument := req.Params.Arguments
logger.DebugContext(ctx, fmt.Sprintf("tool name: %s", toolName))
tool, ok := tools[toolName]
if !ok {
err = fmt.Errorf("invalid tool name: tool with name %q does not exist", toolName)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
// marshal arguments and decode it using decodeJSON instead to prevent loss between floats/int.
aMarshal, err := json.Marshal(toolArgument)
if err != nil {
err = fmt.Errorf("unable to marshal tools argument: %w", err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
var data map[string]any
if err = util.DecodeJSON(bytes.NewBuffer(aMarshal), &data); err != nil {
err = fmt.Errorf("unable to decode tools argument: %w", err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
// claimsFromAuth maps the name of the authservice to the claims retrieved from it.
// Since MCP doesn't support auth, an empty map will be use every time.
claimsFromAuth := make(map[string]map[string]any)
params, err := tool.ParseParams(data, claimsFromAuth)
if err != nil {
err = fmt.Errorf("provided parameters were invalid: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
logger.DebugContext(ctx, fmt.Sprintf("invocation params: %s", params))
if !tool.Authorized([]string{}) {
err = fmt.Errorf("unauthorized Tool call: `authRequired` is set for the target Tool")
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
// run tool invocation and generate response.
results, err := tool.Invoke(ctx, params)
if err != nil {
text := TextContent{
Type: "text",
Text: err.Error(),
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
}
content := make([]TextContent, 0)
for _, d := range results {
text := TextContent{Type: "text"}
dM, err := json.Marshal(d)
if err != nil {
text.Text = fmt.Sprintf("fail to marshal: %s, result: %s", err, d)
} else {
text.Text = string(dM)
}
content = append(content, text)
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: content},
}, nil
}

View File

@@ -1,180 +0,0 @@
// 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 v20250618
import (
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
)
// SERVER_NAME is the server name used in Implementation.
const SERVER_NAME = "Toolbox"
// PROTOCOL_VERSION is the version of the MCP protocol in this package.
const PROTOCOL_VERSION = "2025-06-18"
// methods that are supported.
const (
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
)
/* Empty result */
// EmptyResult represents a response that indicates success but carries no data.
type EmptyResult jsonrpc.Result
/* Pagination */
// Cursor is an opaque token used to represent a cursor for pagination.
type Cursor string
type PaginatedRequest struct {
jsonrpc.Request
Params struct {
// An opaque token representing the current pagination position.
// If provided, the server should return results starting after this cursor.
Cursor Cursor `json:"cursor,omitempty"`
} `json:"params,omitempty"`
}
type PaginatedResult struct {
jsonrpc.Result
// An opaque token representing the pagination position after the last returned result.
// If present, there may be more results available.
NextCursor Cursor `json:"nextCursor,omitempty"`
}
/* Tools */
// Sent from the client to request a list of tools the server has.
type ListToolsRequest struct {
PaginatedRequest
}
// The server's response to a tools/list request from the client.
type ListToolsResult struct {
PaginatedResult
Tools []tools.McpManifest `json:"tools"`
}
// Used by the client to invoke a tool provided by the server.
type CallToolRequest struct {
jsonrpc.Request
Params struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
} `json:"params,omitempty"`
}
// The sender or recipient of messages and data in a conversation.
type Role string
const (
RoleUser Role = "user"
RoleAssistant Role = "assistant"
)
// Base for objects that include optional annotations for the client.
// The client can use annotations to inform how objects are used or displayed
type Annotated struct {
Annotations *struct {
// Describes who the intended customer of this object or data is.
// It can include multiple entries to indicate content useful for multiple
// audiences (e.g., `["user", "assistant"]`).
Audience []Role `json:"audience,omitempty"`
// Describes how important this data is for operating the server.
//
// A value of 1 means "most important," and indicates that the data is
// effectively required, while 0 means "least important," and indicates that
// the data is entirely optional.
//
// @TJS-type number
// @minimum 0
// @maximum 1
Priority float64 `json:"priority,omitempty"`
} `json:"annotations,omitempty"`
}
// TextContent represents text provided to or from an LLM.
type TextContent struct {
Annotated
Type string `json:"type"`
// The text content of the message.
Text string `json:"text"`
}
// The server's response to a tool call.
//
// Any errors that originate from the tool SHOULD be reported inside the result
// object, with `isError` set to true, _not_ as an MCP protocol-level error
// response. Otherwise, the LLM would not be able to see that an error occurred
// and self-correct.
//
// However, any errors in _finding_ the tool, an error indicating that the
// server does not support tool calls, or any other exceptional conditions,
// should be reported as an MCP error response.
type CallToolResult struct {
jsonrpc.Result
// Could be either a TextContent, ImageContent, or EmbeddedResources
// For Toolbox, we will only be sending TextContent
Content []TextContent `json:"content"`
// Whether the tool call ended in an error.
// If not set, this is assumed to be false (the call was successful).
//
// Any errors that originate from the tool SHOULD be reported inside the result
// object, with `isError` set to true, _not_ as an MCP protocol-level error
// response. Otherwise, the LLM would not be able to see that an error occurred
// and self-correct.
//
// However, any errors in _finding_ the tool, an error indicating that the
// server does not support tool calls, or any other exceptional conditions,
// should be reported as an MCP error response.
IsError bool `json:"isError,omitempty"`
// An optional JSON object that represents the structured result of the tool call.
StructuredContent map[string]any `json:"structuredContent,omitempty"`
}
// Additional properties describing a Tool to clients.
//
// NOTE: all properties in ToolAnnotations are **hints**.
// They are not guaranteed to provide a faithful description of
// tool behavior (including descriptive properties like `title`).
//
// Clients should never make tool use decisions based on ToolAnnotations
// received from untrusted servers.
type ToolAnnotations struct {
// A human-readable title for the tool.
Title string `json:"title,omitempty"`
// If true, the tool does not modify its environment.
// Default: false
ReadOnlyHint bool `json:"readOnlyHint,omitempty"`
// If true, the tool may perform destructive updates to its environment.
// If false, the tool performs only additive updates.
// (This property is meaningful only when `readOnlyHint == false`)
// Default: true
DestructiveHint bool `json:"destructiveHint,omitempty"`
// If true, calling the tool repeatedly with the same arguments
// will have no additional effect on the its environment.
// (This property is meaningful only when `readOnlyHint == false`)
// Default: false
IdempotentHint bool `json:"idempotentHint,omitempty"`
// If true, this tool may interact with an "open world" of external
// entities. If false, the tool's domain of interaction is closed.
// For example, the world of a web search tool is open, whereas that
// of a memory tool is not.
// Default: true
OpenWorldHint bool `json:"openWorldHint,omitempty"`
}

View File

@@ -36,7 +36,6 @@ import (
const jsonrpcVersion = "2.0"
const protocolVersion20241105 = "2024-11-05"
const protocolVersion20250326 = "2025-03-26"
const protocolVersion20250618 = "2025-06-18"
const serverName = "Toolbox"
var tool1InputSchema = map[string]any{
@@ -255,7 +254,7 @@ func TestMcpEndpoint(t *testing.T) {
initWant map[string]any
}{
{
name: "version 2024-11-05",
name: "verson 2024-11-05",
protocol: protocolVersion20241105,
idHeader: false,
initWant: map[string]any{
@@ -271,7 +270,7 @@ func TestMcpEndpoint(t *testing.T) {
},
},
{
name: "version 2025-03-26",
name: "verson 2025-03-26",
protocol: protocolVersion20250326,
idHeader: true,
initWant: map[string]any{
@@ -286,22 +285,6 @@ func TestMcpEndpoint(t *testing.T) {
},
},
},
{
name: "version 2025-06-18",
protocol: protocolVersion20250618,
idHeader: false,
initWant: map[string]any{
"jsonrpc": "2.0",
"id": "mcp-initialize",
"result": map[string]any{
"protocolVersion": "2025-06-18",
"capabilities": map[string]any{
"tools": map[string]any{"listChanged": false},
},
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
},
},
},
}
for _, vtc := range versTestCases {
t.Run(vtc.name, func(t *testing.T) {
@@ -312,10 +295,6 @@ func TestMcpEndpoint(t *testing.T) {
header["Mcp-Session-Id"] = sessionId
}
if vtc.protocol == protocolVersion20250618 {
header["MCP-Protocol-Version"] = vtc.protocol
}
testCases := []struct {
name string
url string
@@ -502,7 +481,7 @@ func TestMcpEndpoint(t *testing.T) {
t.Fatalf("unexpected error during marshaling of body")
}
if vtc.protocol != protocolVersion20241105 && len(header) == 0 {
if vtc.protocol == protocolVersion20250326 && len(header) == 0 {
t.Fatalf("header is missing")
}
@@ -535,33 +514,6 @@ func TestMcpEndpoint(t *testing.T) {
}
}
func TestInvalidProtocolVersionHeader(t *testing.T) {
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
header := map[string]string{}
header["MCP-Protocol-Version"] = "foo"
resp, body, err := runRequest(ts, http.MethodPost, "/", nil, header)
if resp.Status != "400 Bad Request" {
t.Fatalf("unexpected status: %s", resp.Status)
}
var got map[string]any
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("unexpected error unmarshalling body: %s", err)
}
want := "invalid protocol version: foo"
if got["error"] != want {
t.Fatalf("unexpected error message: got %s, want %s", got["error"], want)
}
if err != nil {
t.Fatalf("unexpected error during request: %s", err)
}
}
func TestDeleteEndpoint(t *testing.T) {
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)

View File

@@ -330,6 +330,13 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
return nil, err
}
r.Mount("/mcp", mcpR)
if cfg.UI {
webR, err := webRouter()
if err != nil {
return nil, err
}
r.Mount("/ui", webR)
}
// default endpoint for validating server is running
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("🧰 Hello, World! 🧰"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,231 @@
body {
display: flex;
height: 100vh;
margin: 0;
font-family: 'Trebuchet MS';
background-color: #f8f9fa;
}
.left-nav {
flex: 0 0 200px;
background-color: #fff;
box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15);
z-index: 10;
display: flex;
flex-direction: column;
padding: 15px;
align-items: center;
}
.second-nav {
flex: 0 0 250px;
background-color: #fff;
box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15);
z-index: 10;
display: flex;
flex-direction: column;
padding: 15px;
align-items: center;
}
.nav-logo {
width: 100%;
margin-bottom: 20px;
}
.nav-logo img {
max-width: 100%;
height: auto;
display: block;
}
.left-nav ul {
font-family: 'Verdana';
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
.left-nav ul li {
margin-bottom: 5px;
}
.left-nav ul li a {
display: flex;
align-items: center;
padding: 12px;
text-decoration: none;
color: #333;
border-radius: 0;
}
.left-nav ul li a:hover {
background-color: #e9e9e9;
border-radius: 35px;
}
.left-nav ul li a.active {
background-color: #d0d0d0;
font-weight: bold;
border-radius: 35px;
}
.main-content-area {
flex: 1;
display: flex;
flex-direction: column;
}
.top-bar {
background-color: #fff;
padding: 30px 30px;
display: flex;
justify-content: flex-end;
align-items: center;
border-bottom: 1px solid #eee;
}
.top-bar span {
font-weight: bold;
font-size: 1.2em;
font-family: 'Trebuchet MS';
color: #333;
}
.content {
padding: 20px;
flex-grow: 1;
overflow-y: auto;
}
.tool-button {
/* --- Mimicking .left-nav ul li a styles --- */
display: flex;
align-items: center;
padding: 12px;
text-decoration: none;
color: #333;
background-color: transparent; /* Reset default button background */
border: none; /* Reset default button border */
border-radius: 0; /* Start with sharp corners */
width: 100%;
text-align: left;
cursor: pointer;
font-family: inherit; /* Inherit 'Verdana' from ul */
font-size: inherit; /* Inherit font size */
/* Transition to match a tags */
transition: background-color 0.1s ease-in-out, border-radius 0.1s ease-in-out;
}
.tool-button:hover {
/* Mimic .left-nav ul li a:hover */
background-color: #e9e9e9;
border-radius: 35px;
}
.tool-button:focus {
outline: none;
/* Optional: You might want a subtle focus indicator, e.g.
box-shadow: 0 0 0 2px rgba(208, 208, 208, 0.5); */
}
.tool-button.active {
/* Mimic .left-nav ul li a.active */
background-color: #d0d0d0;
font-weight: bold;
border-radius: 35px;
}
.tool-button.active:hover {
background-color: #d0d0d0; /* Keep active color when hovered */
}
#secondary-panel-content ul {
list-style: none; /* This is the main property to remove bullet points */
padding: 0; /* Reset default browser padding on the ul */
margin: 0; /* Reset default browser margin on the ul */
width: 100%; /* Ensure it takes full width if needed */
}
.tool-details-grid {
display: grid;
grid-template-columns: 2fr 1fr; /* 2/3 for info, 1/3 for params */
gap: 20px;
margin-bottom: 20px;
}
.tool-info {
display: flex;
flex-direction: column;
gap: 15px;
}
.tool-params {
background-color: #f9f9f9;
padding: 15px;
border-radius: 4px;
border: 1px solid #ddd;
}
.tool-box {
background-color: #fff;
padding: 15px;
border-radius: 4px;
border: 1px solid #eee;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.tool-box h5 {
margin-top: 0;
margin-bottom: 8px;
font-weight: bold;
}
.param-item {
margin-bottom: 12px;
}
.param-item label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
.param-item input[type="text"],
.param-item input[type="number"],
.param-item select {
width: calc(100% - 12px);
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
}
.run-tool-btn {
padding: 10px 15px;
background-color: #4285f4; /* Google Blue */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
margin-top: 10px;
}
.run-tool-btn:hover {
background-color: #357ae8;
}
.tool-response {
width: 100%;
margin-top: 20px;
}
.tool-response textarea {
width: calc(100% - 24px);
min-height: 150px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
}

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox UI</title>
<link rel="stylesheet" href="/ui/css/style.css">
</head>
<body>
<nav class="left-nav">
<div class="nav-logo">
<img src="/ui/assets/mcptoolboxlogo.png" alt="App Logo">
</div>
<ul>
<li><a href="/ui/sources">Sources</a></li>
<li><a href="/ui/authservices">Auth Services</a></li>
<li><a href="/ui/tools">Tools</a></li>
<li><a href="/ui/toolsets">Toolsets</a></li>
</ul>
</nav>
<div class="main-content-area", align="center">
<div class="top-bar">
</div>
<main class="content">
<h1>Homepage</h1>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,255 @@
// helper function to create form inputs for parameters
function createParamInput(param, toolId) {
const paramItem = document.createElement('div');
paramItem.className = 'param-item';
const label = document.createElement('label');
const inputId = `param-${toolId}-${param.name}`;
label.setAttribute('for', inputId);
label.textContent = param.label;
paramItem.appendChild(label);
let inputElement;
if (param.type === 'select') {
inputElement = document.createElement('select');
param.options.forEach(optionValue => {
const option = document.createElement('option');
option.value = optionValue;
option.textContent = optionValue;
if (optionValue === param.defaultValue) {
option.selected = true;
}
inputElement.appendChild(option);
});
} else if (param.type === 'textarea') { // Handle textarea for arrays
inputElement = document.createElement('textarea');
inputElement.rows = 3;
inputElement.value = param.defaultValue || '';
if (param.valueType && param.valueType.startsWith('array')) {
inputElement.placeholder = 'E.g., ["item1", "item2"] or [1, 2, 3]';
}
} else { // text, number, etc.
inputElement = document.createElement('input');
inputElement.type = param.type;
inputElement.value = param.defaultValue || '';
}
// Common properties
inputElement.id = inputId;
inputElement.name = param.name;
paramItem.appendChild(inputElement);
return paramItem;
}
function displayResults(results, responseArea, prettify) {
if (results === null || results === undefined) {
// responseArea.value = ''; // Keep placeholder or old error message
return;
}
try {
if (prettify) {
responseArea.value = JSON.stringify(JSON.parse(results.result), null, 2);
} else {
responseArea.value = JSON.stringify(JSON.parse(results.result));
}
} catch (error) {
console.error("Error stringifying results:", error);
responseArea.value = "Error displaying results.";
}
}
// function to run the tool (calls API version of endpoint)
async function handleRunTool(toolId, form, responseArea, parameters, prettifyCheckbox, updateLastResults) {
responseArea.value = 'Running tool...';
updateLastResults(null); // Clear last results before new run
const formData = new FormData(form);
const typedParams = {};
for (const param of parameters) {
const rawValue = formData.get(param.name);
if (rawValue === null || rawValue === undefined || rawValue === '') {
if (param.required) {
console.warn(`Required parameter ${param.name} is missing.`);
}
typedParams[param.name] = null;
continue;
}
const valueType = param.valueType;
try {
if (valueType && valueType.startsWith('array<')) {
const elementType = valueType.substring(6, valueType.length - 1);
let parsedArray;
try {
parsedArray = JSON.parse(rawValue);
} catch (e) {
throw new Error(`Invalid JSON format for ${param.name}. ${e.message}`);
}
if (!Array.isArray(parsedArray)) {
throw new Error(`Input for ${param.name} must be a JSON array (e.g., ["a", "b"]).`);
}
if (elementType === 'number') {
typedParams[param.name] = parsedArray.map((item, index) => {
const num = Number(item);
if (isNaN(num)) {
throw new Error(`Invalid number "${item}" found in array for ${param.name} at index ${index}.`);
}
return num;
});
} else if (elementType === 'boolean') {
typedParams[param.name] = parsedArray.map(item => item === true || String(item).toLowerCase() === 'true');
} else { // string or other types
typedParams[param.name] = parsedArray;
}
} else {
switch (valueType) {
case 'number':
const num = Number(rawValue);
if (isNaN(num)) {
throw new Error(`Invalid number input for ${param.name}: ${rawValue}`);
}
typedParams[param.name] = num;
break;
case 'boolean':
typedParams[param.name] = rawValue === 'true';
break;
case 'string':
default:
typedParams[param.name] = rawValue;
break;
}
}
} catch (error) {
console.error('Error processing parameter:', param.name, error);
responseArea.value = `Error for ${param.name}: ${error.message}`;
return; // Stop processing
}
}
console.log('Running tool:', toolId, 'with typed params:', typedParams);
try {
const response = await fetch(`/api/tool/${toolId}/invoke`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(typedParams)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP error ${response.status}: ${errorBody}`);
}
const results = await response.json();
updateLastResults(results); // Update the stored results
displayResults(results, responseArea, prettifyCheckbox.checked); // Display formatted results
} catch (error) {
console.error('Error running tool:', error);
responseArea.value = `Error: ${error.message}`;
updateLastResults(null); // Clear results on error
}
}
// renders the tool display area
export function renderToolInterface(tool, containerElement) {
containerElement.innerHTML = '';
const toolId = tool.id;
let lastResults = null; // Store the most recent successful result object
// Function to update lastResults, closure to keep it private to this scope
const updateLastResults = (newResults) => {
lastResults = newResults;
};
const gridContainer = document.createElement('div');
gridContainer.className = 'tool-details-grid';
const toolInfoContainer = document.createElement('div');
toolInfoContainer.className = 'tool-info';
const nameBox = document.createElement('div');
nameBox.className = 'tool-box tool-name';
nameBox.innerHTML = `<h5>Name:</h5><p>${tool.name}</p>`;
toolInfoContainer.appendChild(nameBox);
const descBox = document.createElement('div');
descBox.className = 'tool-box tool-description';
descBox.innerHTML = `<h5>Description:</h5><p>${tool.description}</p>`;
toolInfoContainer.appendChild(descBox);
gridContainer.appendChild(toolInfoContainer);
const paramsContainer = document.createElement('div');
paramsContainer.className = 'tool-params tool-box';
paramsContainer.innerHTML = '<h5>Parameters:</h5>';
const form = document.createElement('form');
form.id = `tool-params-form-${toolId}`;
tool.parameters.forEach(param => {
form.appendChild(createParamInput(param, toolId));
});
paramsContainer.appendChild(form);
const runButton = document.createElement('button');
runButton.className = 'run-tool-btn';
runButton.textContent = 'Run Tool';
paramsContainer.appendChild(runButton);
gridContainer.appendChild(paramsContainer);
containerElement.appendChild(gridContainer);
// Response Area
const responseContainer = document.createElement('div');
responseContainer.className = 'tool-response tool-box';
const responseHeader = document.createElement('h5');
responseHeader.textContent = 'Response:';
responseContainer.appendChild(responseHeader);
// Prettify Checkbox
const prettifyId = `prettify-${toolId}`;
const prettifyLabel = document.createElement('label');
prettifyLabel.setAttribute('for', prettifyId);
prettifyLabel.textContent = 'Prettify JSON';
prettifyLabel.style.display = 'inline-block';
prettifyLabel.style.marginLeft = '10px';
prettifyLabel.style.verticalAlign = 'middle';
prettifyLabel.style.cursor = 'pointer';
const prettifyCheckbox = document.createElement('input');
prettifyCheckbox.type = 'checkbox';
prettifyCheckbox.id = prettifyId;
prettifyCheckbox.checked = true; // Default to pretty
prettifyCheckbox.style.verticalAlign = 'middle';
prettifyCheckbox.style.cursor = 'pointer';
const prettifyDiv = document.createElement('div');
prettifyDiv.style.marginBottom = '5px';
prettifyDiv.appendChild(prettifyCheckbox);
prettifyDiv.appendChild(prettifyLabel);
responseContainer.appendChild(prettifyDiv);
const responseAreaId = `tool-response-area-${toolId}`;
const responseArea = document.createElement('textarea');
responseArea.id = responseAreaId;
responseArea.readOnly = true;
responseArea.placeholder = 'Results will appear here...';
responseArea.style.width = 'calc(100% - 12px)';
responseArea.rows = 10;
responseContainer.appendChild(responseArea);
containerElement.appendChild(responseContainer);
// Event Listeners
prettifyCheckbox.addEventListener('change', () => {
if (lastResults) {
displayResults(lastResults, responseArea, prettifyCheckbox.checked);
}
});
runButton.addEventListener('click', (event) => {
event.preventDefault();
handleRunTool(toolId, form, responseArea, tool.parameters, prettifyCheckbox, updateLastResults);
});
}

View File

@@ -0,0 +1,156 @@
import { renderToolInterface } from "./toolDisplay.js";
document.addEventListener('DOMContentLoaded', () => {
const toolDisplayArea = document.getElementById('tool-display-area');
const secondaryPanelContent = document.getElementById('secondary-panel-content');
if (!secondaryPanelContent || !toolDisplayArea) {
console.error('Required DOM elements not found.');
return;
}
/**
* Fetches the list of tools from the API and renders them in the secondary panel.
*/
async function loadTools() {
secondaryPanelContent.innerHTML = '<p>Fetching tools...</p>';
try {
// This endpoint should list tools, the structure you provided seems to be for a single tool
const response = await fetch('/api/toolset');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const apiResponse = await response.json();
renderToolList(apiResponse);
} catch (error) {
console.error('Failed to load tools:', error);
secondaryPanelContent.innerHTML = '<p class="error">Failed to load tools. Please try again later.</p>';
}
}
/**
* Renders the list of tools in the secondary navigation panel.
* @param {object} apiResponse - The parsed JSON response from the API.
*/
function renderToolList(apiResponse) {
secondaryPanelContent.innerHTML = '';
if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) {
console.error('Error: Expected an object with a "tools" property, but received:', apiResponse);
secondaryPanelContent.textContent = 'Error: Invalid response format from toolset API.';
return;
}
const toolsObject = apiResponse.tools;
const toolNames = Object.keys(toolsObject);
if (toolNames.length === 0) {
secondaryPanelContent.textContent = 'No tools found.';
return;
}
const ul = document.createElement('ul');
toolNames.forEach(toolName => {
const li = document.createElement('li');
const button = document.createElement('button');
button.textContent = toolName;
button.dataset.toolname = toolName;
button.classList.add('tool-button');
button.addEventListener('click', handleToolClick);
li.appendChild(button);
ul.appendChild(li);
});
secondaryPanelContent.appendChild(ul);
}
/**
* Handles the click event on a tool button in the secondary panel.
* @param {MouseEvent} event - The click event.
*/
function handleToolClick(event) {
const toolName = event.target.dataset.toolname;
if (toolName) {
const currentActive = secondaryPanelContent.querySelector('.tool-button.active');
if (currentActive) {
currentActive.classList.remove('active');
}
event.target.classList.add('active');
fetchToolDetails(toolName);
}
}
/**
* Fetches details for a specific tool from the API and renders the UI.
* @param {string} toolName - The name of the tool.
*/
async function fetchToolDetails(toolName) {
toolDisplayArea.innerHTML = '<p>Loading tool details...</p>';
try {
const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const apiResponse = await response.json();
if (!apiResponse.tools || !apiResponse.tools[toolName]) {
throw new Error(`Tool "${toolName}" data not found in API response.`);
}
const toolObject = apiResponse.tools[toolName];
const toolInterfaceData = {
id: toolName,
name: toolName,
description: toolObject.description || "No description provided.",
parameters: (toolObject.parameters || []).map(param => {
let inputType = 'text'; // Default HTML input type
let options;
const apiType = param.type ? param.type.toLowerCase() : 'string';
let valueType = 'string'; // Data type for API payload
let label = param.description || param.name;
if (apiType === 'integer' || apiType === 'number') {
inputType = 'number';
valueType = 'number';
} else if (apiType === 'boolean') {
inputType = 'select';
options = ['true', 'false'];
valueType = 'boolean';
} else if (apiType === 'array') {
inputType = 'textarea'; // Use textarea for array inputs
const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
valueType = `array<${itemType}>`;
label += ' (JSON Array string)'; // Hint to the user
} else if (param.enum && Array.isArray(param.enum)) {
inputType = 'select';
options = param.enum;
valueType = 'string';
}
console.log(param.name, inputType, label, apiType, valueType)
return {
name: param.name,
type: inputType, // For HTML input element type
valueType: valueType, // For API request payload type
label: label,
required: param.required || false,
options: options,
defaultValue: param.default,
};
})
};
console.log("Transformed toolInterfaceData:", toolInterfaceData);
renderToolInterface(toolInterfaceData, toolDisplayArea);
} catch (error) {
console.error(`Failed to load details for tool "${toolName}":`, error);
toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${toolName}. ${error.message}</p>`;
}
}
// Initial load of tools list
loadTools();
});

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Three</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="left-nav">
<div class="nav-logo">
<img src="/ui/assets/mcptoolboxlogo.png" alt="App Logo">
</div>
<ul>
<li><a href="/ui/sources">Sources</a></li>
<li><a href="/ui/authservices">Auth Services</a></li>
<li><a href="/ui/tools" class="active">Tools</a></li>
<li><a href="/ui/toolsets">Toolsets</a></li>
</ul>
</nav>
<aside class="second-nav">
<h4>My Tools</h4>
<div id="secondary-panel-content">
<p>Fetching tools...</p>
</div>
</aside>
<div class="main-content-area">
<div class="top-bar">
<span>Toolbox UI Inspector</span>
</div>
<main class="content" id="tool-display-area">
<h1>Welcome to the Main Page</h1>
<p>This is the main content area. Click a tab on the left to navigate.</p>
</main>
</div>
<script type="module" src="js/tools.js"></script>
</body>
</html>

53
internal/server/web.go Normal file
View File

@@ -0,0 +1,53 @@
package server
import (
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
//go:embed all:static
var staticContent embed.FS
// webRouter creates a router that represents the routes under /ui
func webRouter() (chi.Router, error) {
r := chi.NewRouter()
r.Use(middleware.StripSlashes)
// direct routes for html pages to provide clean URLs
r.Get("/", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/index.html") })
r.Get("/tools", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/tools.html") })
// handler for all other static files/assets
staticFS, _ := fs.Sub(staticContent, "static")
r.Handle("/*", http.StripPrefix("/ui", http.FileServer(http.FS(staticFS))))
return r, nil
}
func serveHTML(w http.ResponseWriter, r *http.Request, filepath string) {
file, err := staticContent.Open(filepath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer file.Close()
fileBytes, err := io.ReadAll(file)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
return
}
fileInfo, err := file.Stat()
if err != nil {
return
}
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), bytes.NewReader(fileBytes))
}

View File

@@ -0,0 +1,77 @@
package server
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-goquery/goquery"
)
func TestWebEndpoint(t *testing.T) {
router, err := webRouter()
if err != nil {
t.Fatalf("Failed to create webRouter: %v", err)
}
ts := httptest.NewServer(router)
defer ts.Close()
testCases := []struct {
name string
method string
path string
wantStatus int
wantContentType string
wantPageTitle string
}{
{
name: "web index page GET",
method: http.MethodGet,
path: "/",
wantStatus: http.StatusOK,
wantContentType: "text/html; charset=utf-8",
wantPageTitle: "Toolbox UI",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
url := ts.URL + tc.path
req, err := http.NewRequest(tc.method, url, nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Failed to send request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != tc.wantStatus {
t.Errorf("Unexpected status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatus, string(body))
}
if contentType := resp.Header.Get("Content-Type"); contentType != tc.wantContentType {
t.Errorf("Unexpected Content-Type header: got %s, want %s", contentType, tc.wantContentType)
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
if err != nil {
t.Fatalf("Failed to parse HTML: %v", err)
}
gotPageTitle := doc.Find("title").Text()
if gotPageTitle != tc.wantPageTitle {
t.Errorf("Unexpected page title: got %q, want %q", gotPageTitle, tc.wantPageTitle)
}
})
}
}

View File

@@ -18,7 +18,6 @@ import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/goccy/go-yaml"
@@ -46,14 +45,13 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Host string `yaml:"host" validate:"required"`
Port string `yaml:"port" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password" validate:"required"`
Database string `yaml:"database" validate:"required"`
QueryTimeout string `yaml:"queryTimeout"`
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Host string `yaml:"host" validate:"required"`
Port string `yaml:"port" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password" validate:"required"`
Database string `yaml:"database" validate:"required"`
}
func (r Config) SourceConfigKind() string {
@@ -61,7 +59,7 @@ func (r Config) SourceConfigKind() string {
}
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
pool, err := initMySQLConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryTimeout)
pool, err := initMySQLConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database)
if err != nil {
return nil, fmt.Errorf("unable to create pool: %w", err)
}
@@ -95,7 +93,7 @@ func (s *Source) MySQLPool() *sql.DB {
return s.Pool
}
func initMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname, queryTimeout string) (*sql.DB, error) {
func initMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string) (*sql.DB, error) {
//nolint:all // Reassigned ctx
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
defer span.End()
@@ -103,15 +101,6 @@ func initMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, hos
// Configure the driver to connect to the database
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname)
// Add query timeout to DSN if specified
if queryTimeout != "" {
timeout, err := time.ParseDuration(queryTimeout)
if err != nil {
return nil, fmt.Errorf("invalid queryTimeout %q: %w", queryTimeout, err)
}
dsn += "&readTimeout=" + timeout.String()
}
// Interact with the driver directly as you normally would
pool, err := sql.Open("mysql", dsn)
if err != nil {

View File

@@ -54,32 +54,6 @@ func TestParseFromYamlCloudSQLMySQL(t *testing.T) {
},
},
},
{
desc: "with query timeout",
in: `
sources:
my-mysql-instance:
kind: mysql
host: 0.0.0.0
port: my-port
database: my_db
user: my_user
password: my_pass
queryTimeout: 45s
`,
want: server.SourceConfigs{
"my-mysql-instance": mysql.Config{
Name: "my-mysql-instance",
Kind: mysql.SourceKind,
Host: "0.0.0.0",
Port: "my-port",
Database: "my_db",
User: "my_user",
Password: "my_pass",
QueryTimeout: "45s",
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {

View File

@@ -28,7 +28,7 @@ import (
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// setupOTelSDK bootstraps the OpenTelemetry pipeline.

View File

@@ -139,17 +139,13 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, erro
// BigQuery's QueryParameter only accepts typed slices as input
// This checks if the param is an array.
// If yes, convert []any to typed slice (e.g []string, []int)
switch arrayParam := p.(type) {
case *tools.ArrayParameter:
arrayParamValue, ok := value.([]any)
if !ok {
return nil, fmt.Errorf("unable to convert parameter `%s` to []any %w", name, err)
}
itemType := arrayParam.GetItems().GetType()
switch arrayParam := value.(type) {
case []any:
var err error
value, err = tools.ConvertAnySliceToTyped(arrayParamValue, itemType)
itemType := p.McpManifest().Items.Type
value, err = convertAnySliceToTyped(arrayParam, itemType, name)
if err != nil {
return nil, fmt.Errorf("unable to convert parameter `%s` from []any to typed slice: %w", name, err)
return nil, fmt.Errorf("unable to convert []any to typed slice: %w", err)
}
}
@@ -209,3 +205,47 @@ func (t Tool) McpManifest() tools.McpManifest {
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}
func convertAnySliceToTyped(s []any, itemType, paramName string) (any, error) {
var typedSlice any
switch itemType {
case "string":
typedSlice := make([]string, len(s))
for j, item := range s {
if s, ok := item.(string); ok {
typedSlice[j] = s
} else {
return nil, fmt.Errorf("parameter '%s': expected item at index %d to be string, got %T", paramName, j, item)
}
}
case "integer":
typedSlice := make([]int64, len(s))
for j, item := range s {
i, ok := item.(int)
if !ok {
return nil, fmt.Errorf("parameter '%s': expected item at index %d to be integer, got %T", paramName, j, item)
}
typedSlice[j] = int64(i)
}
case "float":
typedSlice := make([]float64, len(s))
for j, item := range s {
if f, ok := item.(float64); ok {
typedSlice[j] = f
} else {
return nil, fmt.Errorf("parameter '%s': expected item at index %d to be float, got %T", paramName, j, item)
}
}
case "boolean":
typedSlice := make([]bool, len(s))
for j, item := range s {
if b, ok := item.(bool); ok {
typedSlice[j] = b
} else {
return nil, fmt.Errorf("parameter '%s': expected item at index %d to be boolean, got %T", paramName, j, item)
}
}
}
return typedSlice, nil
}

View File

@@ -122,43 +122,29 @@ type Tool struct {
mcpManifest tools.McpManifest
}
func getBigtableType(paramType string) (bigtable.SQLType, error) {
switch paramType {
case "boolean":
return bigtable.BoolSQLType{}, nil
case "string":
return bigtable.StringSQLType{}, nil
case "integer":
return bigtable.Int64SQLType{}, nil
case "float":
return bigtable.Float64SQLType{}, nil
case "array":
return bigtable.ArraySQLType{}, nil
default:
return nil, fmt.Errorf("unknow param type %s", paramType)
}
}
func getMapParamsType(tparams tools.Parameters, params tools.ParamValues) (map[string]bigtable.SQLType, error) {
btParamTypes := make(map[string]bigtable.SQLType)
paramTypeMap := make(map[string]string)
for _, p := range tparams {
if p.GetType() == "array" {
itemType, err := getBigtableType(p.Manifest().Items.Type)
if err != nil {
return nil, err
}
btParamTypes[p.GetName()] = bigtable.ArraySQLType{
ElemType: itemType,
}
continue
}
paramType, err := getBigtableType(p.GetType())
if err != nil {
return nil, err
}
btParamTypes[p.GetName()] = paramType
paramTypeMap[p.GetName()] = p.GetType()
}
return btParamTypes, nil
btParams := make(map[string]bigtable.SQLType)
for _, p := range params {
switch paramTypeMap[p.Name] {
case "boolean":
btParams[p.Name] = bigtable.BoolSQLType{}
case "string":
btParams[p.Name] = bigtable.StringSQLType{}
case "integer":
btParams[p.Name] = bigtable.Int64SQLType{}
case "float":
btParams[p.Name] = bigtable.Float64SQLType{}
case "array":
btParams[p.Name] = bigtable.ArraySQLType{}
}
}
return btParams, nil
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {

View File

@@ -15,7 +15,6 @@
package tools
import (
"fmt"
"regexp"
)
@@ -24,50 +23,3 @@ var validName = regexp.MustCompile(`^[a-zA-Z0-9_-]*$`)
func IsValidName(s string) bool {
return validName.MatchString(s)
}
func ConvertAnySliceToTyped(s []any, itemType string) (any, error) {
var typedSlice any
switch itemType {
case "string":
tempSlice := make([]string, len(s))
for j, item := range s {
s, ok := item.(string)
if !ok {
return nil, fmt.Errorf("expected item at index %d to be string, got %T", j, item)
}
tempSlice[j] = s
}
typedSlice = tempSlice
case "integer":
tempSlice := make([]int64, len(s))
for j, item := range s {
i, ok := item.(int)
if !ok {
return nil, fmt.Errorf("expected item at index %d to be integer, got %T", j, item)
}
tempSlice[j] = int64(i)
}
typedSlice = tempSlice
case "float":
tempSlice := make([]float64, len(s))
for j, item := range s {
f, ok := item.(float64)
if !ok {
return nil, fmt.Errorf("expected item at index %d to be float, got %T", j, item)
}
tempSlice[j] = f
}
typedSlice = tempSlice
case "boolean":
tempSlice := make([]bool, len(s))
for j, item := range s {
b, ok := item.(bool)
if !ok {
return nil, fmt.Errorf("expected item at index %d to be boolean, got %T", j, item)
}
tempSlice[j] = b
}
typedSlice = tempSlice
}
return typedSlice, nil
}

View File

@@ -275,11 +275,7 @@ func getURL(baseURL, path string, pathParams, queryParams tools.Parameters, defa
// Set dynamic query parameters
query := parsedURL.Query()
for _, p := range queryParams {
v := paramsMap[p.GetName()]
if v == nil {
v = ""
}
query.Add(p.GetName(), fmt.Sprintf("%v", v))
query.Add(p.GetName(), fmt.Sprintf("%v", paramsMap[p.GetName()]))
}
parsedURL.RawQuery = query.Encode()
return parsedURL.String(), nil

View File

@@ -32,7 +32,6 @@ const (
typeFloat = "float"
typeBool = "boolean"
typeArray = "array"
typeObject = "object"
)
// ParamValues is an ordered list of ParamValue
@@ -110,17 +109,11 @@ func parseFromAuthService(paramAuthServices []ParamAuthService, claimsMap map[st
return nil, fmt.Errorf("missing or invalid authentication header")
}
// CheckParamRequired checks if a parameter is required based on the required and default field.
func CheckParamRequired(required bool, defaultV any) bool {
return required && defaultV == nil
}
// ParseParams is a helper function for parsing Parameters from an arbitraryJSON object.
func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[string]any) (ParamValues, error) {
params := make([]ParamValue, 0, len(ps))
for _, p := range ps {
var v, newV any
var err error
var v any
paramAuthServices := p.GetAuthServices()
name := p.GetName()
if len(paramAuthServices) == 0 {
@@ -129,23 +122,21 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
v, ok = data[name]
if !ok {
v = p.GetDefault()
// if the parameter is required and no value given, throw an error
if CheckParamRequired(p.GetRequired(), v) {
if v == nil {
return nil, fmt.Errorf("parameter %q is required", name)
}
}
} else {
// parse authenticated parameter
var err error
v, err = parseFromAuthService(paramAuthServices, claimsMap)
if err != nil {
return nil, fmt.Errorf("error parsing authenticated parameter %q: %w", name, err)
}
}
if v != nil {
newV, err = p.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
}
newV, err := p.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
}
params = append(params, ParamValue{Name: name, Value: newV})
}
@@ -257,7 +248,6 @@ type Parameter interface {
GetName() string
GetType() string
GetDefault() any
GetRequired() bool
GetAuthServices() []ParamAuthService
Parse(any) (any, error)
Manifest() ParameterManifest
@@ -368,17 +358,6 @@ func parseParamFromDelayedUnmarshaler(ctx context.Context, u *util.DelayedUnmars
a.AuthSources = nil
}
return a, nil
case typeObject:
a := &ObjectParameter{}
if err := dec.DecodeContext(ctx, a); err != nil {
return nil, fmt.Errorf("unable to parse as %q: %w", t, err)
}
if a.AuthSources != nil {
logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` for parameters instead")
a.AuthServices = append(a.AuthServices, a.AuthSources...)
a.AuthSources = nil
}
return a, nil
}
return nil, fmt.Errorf("%q is not valid type for a parameter", t)
}
@@ -399,7 +378,7 @@ func (ps Parameters) McpManifest() McpToolsSchema {
name := p.GetName()
properties[name] = p.McpManifest()
// parameters that doesn't have a default value are added to the required field
if CheckParamRequired(p.GetRequired(), p.GetDefault()) {
if p.GetDefault() == nil {
required = append(required, name)
}
}
@@ -413,23 +392,19 @@ func (ps Parameters) McpManifest() McpToolsSchema {
// ParameterManifest represents parameters when served as part of a ToolManifest.
type ParameterManifest struct {
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Description string `json:"description"`
AuthServices []string `json:"authSources"`
Items *ParameterManifest `json:"items,omitempty"`
Properties map[string]*ParameterManifest `json:"properties,omitempty"`
AdditionalProperties *ParameterManifest `json:"additionalProperties,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Description string `json:"description"`
AuthServices []string `json:"authSources"`
Items *ParameterManifest `json:"items,omitempty"`
}
// ParameterMcpManifest represents properties when served as part of a ToolMcpManifest.
type ParameterMcpManifest struct {
Type string `json:"type"`
Description string `json:"description"`
Items *ParameterMcpManifest `json:"items,omitempty"`
Properties map[string]*ParameterMcpManifest `json:"properties,omitempty"`
AdditionalProperties *ParameterMcpManifest `json:"addtionalProperties,omitempty"`
Type string `json:"type"`
Description string `json:"description"`
Items *ParameterMcpManifest `json:"items,omitempty"`
}
// CommonParameter are default fields that are emebdding in most Parameter implementations. Embedding this stuct will give the object Name() and Type() functions.
@@ -437,7 +412,6 @@ type CommonParameter struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Desc string `yaml:"description" validate:"required"`
Required *bool `yaml:"required"`
AuthServices []ParamAuthService `yaml:"authServices"`
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
}
@@ -452,15 +426,6 @@ func (p *CommonParameter) GetType() string {
return p.Type
}
// GetRequired returns the type specified for the Parameter.
func (p *CommonParameter) GetRequired() bool {
// parameters are defaulted to required
if p.Required == nil {
return true
}
return *p.Required
}
// McpManifest returns the MCP manifest for the Parameter.
func (p *CommonParameter) McpManifest() ParameterMcpManifest {
return ParameterMcpManifest{
@@ -510,19 +475,6 @@ func NewStringParameterWithDefault(name string, defaultV, desc string) *StringPa
}
}
// NewStringParameterWithRequired is a convenience function for initializing a StringParameter.
func NewStringParameterWithRequired(name string, desc string, required bool) *StringParameter {
return &StringParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeString,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewStringParameterWithAuth is a convenience function for initializing a StringParameter with a list of ParamAuthService.
func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter {
return &StringParameter{
@@ -570,11 +522,11 @@ func (p *StringParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: r,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
@@ -605,19 +557,6 @@ func NewIntParameterWithDefault(name string, defaultV int, desc string) *IntPara
}
}
// NewIntParameterWithRequired is a convenience function for initializing a IntParameter.
func NewIntParameterWithRequired(name string, desc string, required bool) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeInt,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewIntParameterWithAuth is a convenience function for initializing a IntParameter with a list of ParamAuthService.
func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter {
return &IntParameter{
@@ -677,11 +616,11 @@ func (p *IntParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: r,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
@@ -712,19 +651,6 @@ func NewFloatParameterWithDefault(name string, defaultV float64, desc string) *F
}
}
// NewFloatParameterWithRequired is a convenience function for initializing a FloatParameter.
func NewFloatParameterWithRequired(name string, desc string, required bool) *FloatParameter {
return &FloatParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeFloat,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewFloatParameterWithAuth is a convenience function for initializing a FloatParameter with a list of ParamAuthService.
func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter {
return &FloatParameter{
@@ -782,11 +708,11 @@ func (p *FloatParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: r,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
@@ -817,19 +743,6 @@ func NewBooleanParameterWithDefault(name string, defaultV bool, desc string) *Bo
}
}
// NewBooleanParameterWithRequired is a convenience function for initializing a BooleanParameter.
func NewBooleanParameterWithRequired(name string, desc string, required bool) *BooleanParameter {
return &BooleanParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeBool,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewBooleanParameterWithAuth is a convenience function for initializing a BooleanParameter with a list of ParamAuthService.
func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter {
return &BooleanParameter{
@@ -876,11 +789,11 @@ func (p *BooleanParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: r,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
@@ -913,20 +826,6 @@ func NewArrayParameterWithDefault(name string, defaultV []any, desc string, item
}
}
// NewArrayParameterWithRequired is a convenience function for initializing a ArrayParameter with default value.
func NewArrayParameterWithRequired(name string, desc string, required bool, items Parameter) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeArray,
Desc: desc,
Required: &required,
AuthServices: nil,
},
Items: items,
}
}
// NewArrayParameterWithAuth is a convenience function for initializing a ArrayParameter with a list of ParamAuthService.
func NewArrayParameterWithAuth(name string, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
return &ArrayParameter{
@@ -999,10 +898,6 @@ func (p *ArrayParameter) GetDefault() any {
return *p.Default
}
func (p *ArrayParameter) GetItems() Parameter {
return p.Items
}
// Manifest returns the manifest for the ArrayParameter.
func (p *ArrayParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
@@ -1011,13 +906,12 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
authNames[i] = a.Name
}
items := p.Items.Manifest()
// if required value is true, or there's no default value
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
items.Required = r
required := p.Default == nil
items.Required = required
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: r,
Required: required,
Description: p.Desc,
AuthServices: authNames,
Items: &items,
@@ -1038,194 +932,3 @@ func (p *ArrayParameter) McpManifest() ParameterMcpManifest {
Items: &items,
}
}
// NewObjectParameter is a convenience function for initializing a ObjectParameter.
func NewObjectParameter(name string, desc string, properties map[string]Parameter, additionalProperties Parameter) *ObjectParameter {
return &ObjectParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeObject,
Desc: desc,
AuthServices: nil,
},
Properties: properties,
AdditionalProperties: additionalProperties,
}
}
// NewObjectParameterWithDefault is a convenience function for initializing a ObjectParameter with default value.
func NewObjectParameterWithDefault(name string, defaultV map[string]any, desc string, properties map[string]Parameter, additionalProperties Parameter) *ObjectParameter {
return &ObjectParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeObject,
Desc: desc,
AuthServices: nil,
},
Default: &defaultV,
Properties: properties,
AdditionalProperties: additionalProperties,
}
}
// NewObjectParameterWithAuth is a convenience function for initializing a ObjectParameter with a list of ParamAuthService.
func NewObjectParameterWithAuth(name string, desc string, authServices []ParamAuthService, properties map[string]Parameter, additionalProperties Parameter) *ObjectParameter {
return &ObjectParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeObject,
Desc: desc,
AuthServices: authServices,
},
Properties: properties,
AdditionalProperties: additionalProperties,
}
}
var _ Parameter = &ObjectParameter{}
// ObjectParameter is a parameter representing the "map" type.
type ObjectParameter struct {
CommonParameter `yaml:",inline"`
Default *map[string]any `yaml:"default"`
Properties map[string]Parameter `yaml:"properties"`
AdditionalProperties Parameter `yaml:"addtionalProperties"`
}
func (p *ObjectParameter) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
var rawItem struct {
CommonParameter `yaml:",inline"`
Default *map[string]any `yaml:"default"`
Properties map[string]*util.DelayedUnmarshaler `yaml:"properties"`
AdditionalProperties *util.DelayedUnmarshaler `yaml:"additionalProperties"`
}
if err := unmarshal(&rawItem); err != nil {
return err
}
p.CommonParameter = rawItem.CommonParameter
p.Default = rawItem.Default
if rawItem.AdditionalProperties != nil {
param, err := parseParamFromDelayedUnmarshaler(ctx, rawItem.AdditionalProperties)
if err != nil {
return fmt.Errorf("failed to parse additionalProperties: %w", err)
}
p.AdditionalProperties = param
}
if len(rawItem.Properties) == 0 {
return nil
}
p.Properties = make(map[string]Parameter, len(rawItem.Properties))
for key, delayedParam := range rawItem.Properties {
// Parse individual property parameters
param, err := parseParamFromDelayedUnmarshaler(ctx, delayedParam)
if err != nil {
return fmt.Errorf("failed to parse property %q: %w", key, err)
}
p.Properties[key] = param
}
return nil
}
func (p *ObjectParameter) Parse(v any) (any, error) {
objVal, ok := v.(map[string]any)
if !ok {
return nil, &ParseTypeError{p.Name, p.Type, v}
}
parsedObj := make(map[string]any, len(objVal))
for key, val := range objVal {
var (
parsedVal any
err error
)
propertySchema, isDefinedProperty := p.Properties[key]
if isDefinedProperty {
// If the property is explicitly defined in the schema.
parsedVal, err = propertySchema.Parse(val)
if err != nil {
return nil, fmt.Errorf("unable to parse property %q: %w", key, err)
}
} else if p.AdditionalProperties != nil {
// If the property is not defined, but the schema allows additional properties.
parsedVal, err = p.AdditionalProperties.Parse(val)
if err != nil {
return nil, fmt.Errorf("unable to parse additional property %q: %w", key, err)
}
} else {
return nil, fmt.Errorf("unknown property %q found and additional properties are not allowed", key)
}
parsedObj[key] = parsedVal
}
return parsedObj, nil
}
func (p *ObjectParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *ObjectParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
// Manifest returns the manifest for the ObjectParameter.
func (p *ObjectParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
propertiesManifest := make(map[string]*ParameterManifest, len(p.Properties))
for key, p := range p.Properties {
m := p.Manifest()
propertiesManifest[key] = &m
}
var apManifest ParameterManifest
if p.AdditionalProperties != nil {
apManifest = p.AdditionalProperties.Manifest()
}
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
Properties: propertiesManifest,
AdditionalProperties: &apManifest,
}
}
// McpManifest returns the MCP manifest for the ObjectParameter.
func (p *ObjectParameter) McpManifest() ParameterMcpManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
propertiesManifest := make(map[string]*ParameterMcpManifest, len(p.Properties))
for key, p := range p.Properties {
m := p.McpManifest()
propertiesManifest[key] = &m
}
var apManifest ParameterMcpManifest
if p.AdditionalProperties != nil {
apManifest = p.AdditionalProperties.McpManifest()
}
return ParameterMcpManifest{
Type: p.Type,
Description: p.Desc,
Properties: propertiesManifest,
AdditionalProperties: &apManifest,
}
}

View File

@@ -23,7 +23,6 @@ import (
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
)
@@ -51,20 +50,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewStringParameter("my_string", "this param is a string"),
},
},
{
name: "string not required",
in: []map[string]any{
{
"name": "my_string",
"type": "string",
"description": "this param is a string",
"required": false,
},
},
want: tools.Parameters{
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
},
},
{
name: "int",
in: []map[string]any{
@@ -78,20 +63,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewIntParameter("my_integer", "this param is an int"),
},
},
{
name: "int not required",
in: []map[string]any{
{
"name": "my_integer",
"type": "integer",
"description": "this param is an int",
"required": false,
},
},
want: tools.Parameters{
tools.NewIntParameterWithRequired("my_integer", "this param is an int", false),
},
},
{
name: "float",
in: []map[string]any{
@@ -105,20 +76,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewFloatParameter("my_float", "my param is a float"),
},
},
{
name: "float not required",
in: []map[string]any{
{
"name": "my_float",
"type": "float",
"description": "my param is a float",
"required": false,
},
},
want: tools.Parameters{
tools.NewFloatParameterWithRequired("my_float", "my param is a float", false),
},
},
{
name: "bool",
in: []map[string]any{
@@ -132,20 +89,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewBooleanParameter("my_bool", "this param is a boolean"),
},
},
{
name: "bool not required",
in: []map[string]any{
{
"name": "my_bool",
"type": "boolean",
"description": "this param is a boolean",
"required": false,
},
},
want: tools.Parameters{
tools.NewBooleanParameterWithRequired("my_bool", "this param is a boolean", false),
},
},
{
name: "string array",
in: []map[string]any{
@@ -164,25 +107,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
},
},
{
name: "string array not required",
in: []map[string]any{
{
"name": "my_array",
"type": "array",
"description": "this param is an array of strings",
"required": false,
"items": map[string]string{
"name": "my_string",
"type": "string",
"description": "string item",
},
},
},
want: tools.Parameters{
tools.NewArrayParameterWithRequired("my_array", "this param is an array of strings", false, tools.NewStringParameter("my_string", "string item")),
},
},
{
name: "float array",
in: []map[string]any{
@@ -201,31 +125,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewArrayParameter("my_array", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
},
},
{
name: "object",
in: []map[string]any{
{
"name": "my_object",
"type": "object",
"description": "this param is an object",
"properties": map[string]any{
"k1": map[string]any{
"name": "k1",
"type": "float",
"description": "float property",
},
"k2": map[string]any{
"name": "k2",
"type": "string",
"description": "string property",
},
},
},
},
want: tools.Parameters{
tools.NewObjectParameter("my_object", "this param is an object", map[string]tools.Parameter{"k1": tools.NewFloatParameter("k1", "float property"), "k2": tools.NewStringParameter("k2", "string property")}, nil),
},
},
{
name: "string default",
in: []map[string]any{
@@ -320,37 +219,6 @@ func TestParametersMarshal(t *testing.T) {
tools.NewArrayParameterWithDefault("my_array", []any{1.0, 1.1}, "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
},
},
{
name: "object",
in: []map[string]any{
{
"name": "my_object",
"type": "object",
"default": map[string]any{"hello": "world"},
"description": "this param is an object",
"properties": map[string]any{
"k1": map[string]any{
"name": "k1",
"type": "float",
"description": "float property",
},
"k2": map[string]any{
"name": "k2",
"type": "string",
"description": "string property",
},
},
"additionalProperties": map[string]any{
"name": "strProperty",
"type": "string",
"description": "string property",
},
},
},
want: tools.Parameters{
tools.NewObjectParameterWithDefault("my_object", map[string]any{"hello": "world"}, "this param is an object", map[string]tools.Parameter{"k1": tools.NewFloatParameter("k1", "float property"), "k2": tools.NewStringParameter("k2", "string property")}, tools.NewStringParameter("strProperty", "string property")),
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -407,13 +275,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "string with authServices",
name: "string with authSources",
in: []map[string]any{
{
"name": "my_string",
"type": "string",
"description": "this param is a string",
"authServices": []map[string]string{
"authSources": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -453,13 +321,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "int with authServices",
name: "int with authSources",
in: []map[string]any{
{
"name": "my_integer",
"type": "integer",
"description": "this param is an int",
"authServices": []map[string]string{
"authSources": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -499,13 +367,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "float with authServices",
name: "float with authSources",
in: []map[string]any{
{
"name": "my_float",
"type": "float",
"description": "my param is a float",
"authServices": []map[string]string{
"authSources": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -545,13 +413,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "bool with authServices",
name: "bool with authSources",
in: []map[string]any{
{
"name": "my_bool",
"type": "boolean",
"description": "this param is a boolean",
"authServices": []map[string]string{
"authSources": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -596,7 +464,7 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "string array with authServices",
name: "string array with authSources",
in: []map[string]any{
{
"name": "my_array",
@@ -607,7 +475,7 @@ func TestAuthParametersMarshal(t *testing.T) {
"type": "string",
"description": "string item",
},
"authServices": []map[string]string{
"authSources": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -651,46 +519,6 @@ func TestAuthParametersMarshal(t *testing.T) {
tools.NewArrayParameterWithAuth("my_array", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item"), authServices),
},
},
{
name: "object",
in: []map[string]any{
{
"name": "my_object",
"type": "object",
"description": "this param is an object",
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
},
{
"name": "other-auth-service",
"field": "user_id",
},
},
"properties": map[string]any{
"k1": map[string]any{
"name": "k1",
"type": "float",
"description": "float property",
},
"k2": map[string]any{
"name": "k2",
"type": "string",
"description": "string property",
},
},
"additionalProperties": map[string]any{
"name": "strProperty",
"type": "string",
"description": "string property",
},
},
},
want: tools.Parameters{
tools.NewObjectParameterWithAuth("my_object", "this param is an object", authServices, map[string]tools.Parameter{"k1": tools.NewFloatParameter("k1", "float property"), "k2": tools.NewStringParameter("k2", "string property")}, tools.NewStringParameter("strProperty", "string property")),
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -714,11 +542,10 @@ func TestAuthParametersMarshal(t *testing.T) {
func TestParametersParse(t *testing.T) {
tcs := []struct {
name string
params tools.Parameters
in map[string]any
want tools.ParamValues
wantErr bool
name string
params tools.Parameters
in map[string]any
want tools.ParamValues
}{
{
name: "string",
@@ -738,7 +565,6 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_string": 4,
},
wantErr: true,
},
{
name: "int",
@@ -758,17 +584,16 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_int": 14.5,
},
wantErr: true,
},
{
name: "int from json.Number",
name: "not int (big)",
params: tools.Parameters{
tools.NewIntParameter("my_int", "this param is an int"),
},
in: map[string]any{
"my_int": json.Number("9223372036854775807"),
"my_int": math.MaxInt64,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: int(math.MaxInt64)}},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: math.MaxInt64}},
},
{
name: "float",
@@ -788,7 +613,6 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_float": true,
},
wantErr: true,
},
{
name: "bool",
@@ -808,7 +632,6 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_bool": 1.5,
},
wantErr: true,
},
{
name: "string default",
@@ -850,135 +673,44 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
},
{
name: "string not required",
params: tools.Parameters{
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: nil}},
},
{
name: "int not required",
params: tools.Parameters{
tools.NewIntParameterWithRequired("my_int", "this param is an int", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: nil}},
},
{
name: "float not required",
params: tools.Parameters{
tools.NewFloatParameterWithRequired("my_float", "this param is a float", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: nil}},
},
{
name: "bool not required",
params: tools.Parameters{
tools.NewBooleanParameterWithRequired("my_bool", "this param is a bool", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: nil}},
},
{
name: "array of strings",
params: tools.Parameters{
tools.NewArrayParameter("my_array", "an array", tools.NewStringParameter("item", "a string item")),
},
in: map[string]any{"my_array": []any{"a", "b", "c"}},
want: tools.ParamValues{tools.ParamValue{Name: "my_array", Value: []any{"a", "b", "c"}}},
},
{
name: "array with item type mismatch",
params: tools.Parameters{
tools.NewArrayParameter("my_array", "an array", tools.NewIntParameter("item", "an int item")),
},
in: map[string]any{"my_array": []any{1, "b", 3}},
wantErr: true,
},
{
name: "array not required",
params: tools.Parameters{
tools.NewArrayParameterWithRequired("my_array", "an array", false, tools.NewStringParameter("item", "a string item")),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_array", Value: nil}},
},
{
name: "array with default",
params: tools.Parameters{
tools.NewArrayParameterWithDefault("my_array", []any{"x", "y"}, "an array", tools.NewStringParameter("item", "a string item")),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_array", Value: []any{"x", "y"}}},
},
{
name: "object",
params: tools.Parameters{
tools.NewObjectParameter("my_object", "an object", map[string]tools.Parameter{
"key1": tools.NewStringParameter("key1", "string value"),
"key2": tools.NewIntParameter("key2", "int value"),
}, nil),
},
in: map[string]any{"my_object": map[string]any{
"key1": "hello",
"key2": 123,
}},
want: tools.ParamValues{tools.ParamValue{Name: "my_object", Value: map[string]any{
"key1": "hello",
"key2": 123,
}}},
},
{
name: "object with property type mismatch",
params: tools.Parameters{
tools.NewObjectParameter("my_object", "an object", map[string]tools.Parameter{
"key1": tools.NewStringParameter("key1", "string value"),
}, nil),
},
in: map[string]any{"my_object": map[string]any{"key1": 123}},
wantErr: true,
},
{
name: "object with missing required property",
params: tools.Parameters{
tools.NewObjectParameter("my_object", "an object", map[string]tools.Parameter{
"required_key": tools.NewStringParameter("required_key", "a required value"),
}, nil),
},
in: map[string]any{"my_object": map[string]any{"another_key": "foo"}},
wantErr: true,
},
{
name: "object with default",
params: tools.Parameters{
tools.NewObjectParameterWithDefault("my_object", map[string]any{"key1": "default"}, "an object", map[string]tools.Parameter{
"key1": tools.NewStringParameter("key1", "string value"),
}, nil),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_object", Value: map[string]any{"key1": "default"}}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got, err := tools.ParseParams(tc.params, tc.in, make(map[string]map[string]any))
// parse map to bytes
data, err := json.Marshal(tc.in)
if err != nil {
t.Fatalf("unable to marshal input to yaml: %s", err)
}
// parse bytes to object
var m map[string]any
if tc.wantErr {
if err == nil {
t.Fatal("expected error but got nil")
}
return
d := json.NewDecoder(bytes.NewReader(data))
d.UseNumber()
err = d.Decode(&m)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
wantErr := len(tc.want) == 0 // error is expected if no items in want
gotAll, err := tools.ParseParams(tc.params, m, make(map[string]map[string]any))
if err != nil {
if wantErr {
return
}
t.Fatalf("unexpected error from ParseParams: %s", err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("ParseParams() mismatch (-want +got):\n%s", diff)
if wantErr {
t.Fatalf("expected error but Param parsed successfully: %s", gotAll)
}
for i, got := range gotAll {
want := tc.want[i]
if got != want {
t.Fatalf("unexpected value: got %q, want %q", got, want)
}
gotType, wantType := reflect.TypeOf(got), reflect.TypeOf(want)
if gotType != wantType {
t.Fatalf("unexpected value: got %q, want %q", got, want)
}
}
})
}
@@ -1236,27 +968,7 @@ func TestParamManifest(t *testing.T) {
Required: true,
Description: "bar",
AuthServices: []string{},
Items: &tools.ParameterManifest{
Name: "foo-string",
Type: "string",
Required: true,
Description: "bar",
AuthServices: []string{}},
},
},
{
name: "object",
in: tools.NewObjectParameter("foo-object", "bar", map[string]tools.Parameter{"propertyName": tools.NewStringParameter("property", "property desc")}, tools.NewStringParameter("strProperty", "string property")),
want: tools.ParameterManifest{
Name: "foo-object",
Type: "object",
Required: true,
Description: "bar",
AuthServices: []string{},
Properties: map[string]*tools.ParameterManifest{"propertyName": {
Name: "property", Type: "string",
Required: true, Description: "property desc", AuthServices: []string{}}},
AdditionalProperties: &tools.ParameterManifest{Name: "strProperty", Type: "string", Required: true, Description: "string property", AuthServices: []string{}},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}},
},
},
{
@@ -1291,57 +1003,12 @@ func TestParamManifest(t *testing.T) {
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
},
{
name: "string not required",
in: tools.NewStringParameterWithRequired("foo-string", "bar", false),
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "int not required",
in: tools.NewIntParameterWithRequired("foo-int", "bar", false),
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "float not required",
in: tools.NewFloatParameterWithRequired("foo-float", "bar", false),
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "boolean not required",
in: tools.NewBooleanParameterWithRequired("foo-bool", "bar", false),
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "array not required",
in: tools.NewArrayParameterWithRequired("foo-array", "bar", false, tools.NewStringParameter("foo-string", "bar")),
want: tools.ParameterManifest{
Name: "foo-array",
Type: "array",
Required: false,
Description: "bar",
AuthServices: []string{},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
},
{
name: "object default",
in: tools.NewObjectParameterWithDefault("object-default", map[string]any{"hello": "world"}, "bar", map[string]tools.Parameter{"k1": tools.NewFloatParameter("k1", "float property")}, nil),
want: tools.ParameterManifest{
Name: "object-default",
Type: "object",
Required: false,
Description: "bar",
AuthServices: []string{},
Properties: map[string]*tools.ParameterManifest{"k1": {Name: "k1", Type: "float", Required: true, Description: "float property", AuthServices: []string{}}},
AdditionalProperties: &tools.ParameterManifest{},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.Manifest()
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
}
})
}
@@ -1382,22 +1049,12 @@ func TestParamMcpManifest(t *testing.T) {
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
},
{
name: "object",
in: tools.NewObjectParameter("foo-object", "bar", map[string]tools.Parameter{"k1": tools.NewStringParameter("k1", "bar")}, tools.NewStringParameter("p1", "additional property")),
want: tools.ParameterMcpManifest{
Type: "object",
Description: "bar",
Properties: map[string]*tools.ParameterMcpManifest{"k1": {Type: "string", Description: "bar"}},
AdditionalProperties: &tools.ParameterMcpManifest{Type: "string", Description: "additional property"},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.McpManifest()
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
}
})
}
@@ -1410,49 +1067,42 @@ func TestMcpManifest(t *testing.T) {
want tools.McpToolsSchema
}{
{
name: "various parameters",
name: "string",
in: tools.Parameters{
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
tools.NewStringParameter("foo-string2", "bar"),
tools.NewStringParameterWithRequired("foo-string-req", "bar", true),
tools.NewStringParameterWithRequired("foo-string-not-req", "bar", false),
tools.NewIntParameterWithDefault("foo-int", 1, "bar"),
tools.NewIntParameter("foo-int2", "bar"),
tools.NewArrayParameterWithDefault("foo-array", []any{"hello", "world"}, "bar", tools.NewStringParameter("foo-string-item", "bar")),
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string-item2", "bar")),
tools.NewObjectParameter("foo-object", "bar", map[string]tools.Parameter{"k1": tools.NewFloatParameter("k1", "float property")}, nil),
tools.NewArrayParameterWithDefault("foo-array", []any{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "bar")),
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "bar")),
},
want: tools.McpToolsSchema{
Type: "object",
Properties: map[string]tools.ParameterMcpManifest{
"foo-string": {Type: "string", Description: "bar"},
"foo-string2": {Type: "string", Description: "bar"},
"foo-string-req": {Type: "string", Description: "bar"},
"foo-string-not-req": {Type: "string", Description: "bar"},
"foo-int": {Type: "integer", Description: "bar"},
"foo-int2": {Type: "integer", Description: "bar"},
"foo-array": {
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
"foo-int2": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
"foo-array": tools.ParameterMcpManifest{
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
"foo-array2": {
"foo-array2": tools.ParameterMcpManifest{
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
"foo-object": {Type: "object", Description: "bar", Properties: map[string]*tools.ParameterMcpManifest{"k1": {Type: "float", Description: "float property"}}, AdditionalProperties: &tools.ParameterMcpManifest{}},
},
Required: []string{"foo-array2", "foo-int2", "foo-object", "foo-string-req", "foo-string2"},
Required: []string{"foo-string2", "foo-int2", "foo-array2"},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.McpManifest()
opts := cmpopts.SortSlices(func(a, b string) bool { return a < b })
if diff := cmp.Diff(tc.want, got, opts); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
}
})
}
@@ -1852,45 +1502,3 @@ func TestFailResolveTemplateParameters(t *testing.T) {
})
}
}
func TestCheckParamRequired(t *testing.T) {
tcs := []struct {
name string
required bool
defaultV any
want bool
}{
{
name: "required and no default",
required: true,
defaultV: nil,
want: true,
},
{
name: "required and default",
required: true,
defaultV: "foo",
want: false,
},
{
name: "not required and no default",
required: false,
defaultV: nil,
want: false,
},
{
name: "not required and default",
required: false,
defaultV: "foo",
want: false,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tools.CheckParamRequired(tc.required, tc.defaultV)
if got != tc.want {
t.Fatalf("got %v, want %v", got, tc.want)
}
})
}
}

View File

@@ -177,7 +177,6 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
}
// replaceCommandsParams is a helper function to replace parameters in the commands
func replaceCommandsParams(commands [][]string, params tools.Parameters, paramValues tools.ParamValues) ([][]any, error) {
paramMap := paramValues.AsMapWithDollarPrefix()
typeMap := make(map[string]string, len(params))
@@ -187,12 +186,12 @@ func replaceCommandsParams(commands [][]string, params tools.Parameters, paramVa
}
newCommands := make([][]any, len(commands))
for i, cmd := range commands {
newCmd := make([]any, 0)
for _, part := range cmd {
newCmd := make([]any, len(cmd))
for j, part := range cmd {
v, ok := paramMap[part]
if !ok {
// Command part is not a Parameter placeholder
newCmd = append(newCmd, part)
newCmd[j] = part
continue
}
if typeMap[part] == "array" {
@@ -203,7 +202,7 @@ func replaceCommandsParams(commands [][]string, params tools.Parameters, paramVa
}
continue
}
newCmd = append(newCmd, fmt.Sprintf("%s", v))
newCmd[j] = fmt.Sprintf("%s", v)
}
newCommands[i] = newCmd
}

View File

@@ -175,30 +175,6 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, erro
if err != nil {
return nil, fmt.Errorf("unable to extract standard params %w", err)
}
for i, p := range t.Parameters {
name := p.GetName()
value := newParams[i].Value
// Spanner only accepts typed slices as input
// This checks if the param is an array.
// If yes, convert []any to typed slice (e.g []string, []int)
switch arrayParam := p.(type) {
case *tools.ArrayParameter:
arrayParamValue, ok := value.([]any)
if !ok {
return nil, fmt.Errorf("unable to convert parameter `%s` to []any %w", name, err)
}
itemType := arrayParam.GetItems().GetType()
var err error
value, err = tools.ConvertAnySliceToTyped(arrayParamValue, itemType)
if err != nil {
return nil, fmt.Errorf("unable to convert parameter `%s` from []any to typed slice: %w", name, err)
}
}
newParams[i] = tools.ParamValue{Name: name, Value: value}
}
mapParams, err := getMapParams(newParams, t.dialect)
if err != nil {
return nil, fmt.Errorf("fail to get map params: %w", err)

View File

@@ -1,120 +0,0 @@
// 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 wait
import (
"context"
"fmt"
"time"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
)
const kind string = "wait"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Description string `yaml:"description" validate:"required"`
Timeout string `yaml:"timeout" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(_ map[string]sources.Source) (tools.Tool, error) {
durationParameter := tools.NewStringParameter("duration", "The duration to wait for, specified as a string (e.g., '10s', '2m', '1h').")
parameters := tools.Parameters{durationParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string
Kind string
Parameters tools.Parameters
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
paramsMap := params.AsMap()
durationStr, ok := paramsMap["duration"].(string)
if !ok {
return nil, fmt.Errorf("duration parameter is not a string")
}
totalDuration, err := time.ParseDuration(durationStr)
if err != nil {
return nil, fmt.Errorf("invalid duration format: %w", err)
}
time.Sleep(totalDuration)
return []any{fmt.Sprintf("Wait for %v completed successfully.", totalDuration)}, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return true
}

View File

@@ -1,75 +0,0 @@
// 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 wait_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
wait "github.com/googleapis/genai-toolbox/internal/tools/utility/wait"
)
func TestParseFromYamlWait(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
tcs := []struct {
desc string
in string
want server.ToolConfigs
}{
{
desc: "basic example",
in: `
tools:
example_tool:
kind: wait
description: some description
timeout: 10s
authRequired:
- my-google-auth-service
`,
want: server.ToolConfigs{
"example_tool": wait.Config{
Name: "example_tool",
Kind: "wait",
Description: "some description",
Timeout: "10s",
AuthRequired: []string{"my-google-auth-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}

View File

@@ -154,7 +154,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, erro
return out, nil
}
// replaceCommandsParams is a helper function to replace parameters in the commands
// Helper function to replace parameters in the commands
func replaceCommandsParams(commands [][]string, params tools.Parameters, paramValues tools.ParamValues) ([][]string, error) {
paramMap := paramValues.AsMapWithDollarPrefix()
typeMap := make(map[string]string, len(params))
@@ -162,15 +162,14 @@ func replaceCommandsParams(commands [][]string, params tools.Parameters, paramVa
placeholder := "$" + p.GetName()
typeMap[placeholder] = p.GetType()
}
newCommands := make([][]string, len(commands))
for i, cmd := range commands {
newCmd := make([]string, 0)
for _, part := range cmd {
newCmd := make([]string, len(cmd))
for j, part := range cmd {
v, ok := paramMap[part]
if !ok {
// Command part is not a Parameter placeholder
newCmd = append(newCmd, part)
newCmd[j] = part
continue
}
if typeMap[part] == "array" {
@@ -181,7 +180,7 @@ func replaceCommandsParams(commands [][]string, params tools.Parameters, paramVa
}
continue
}
newCmd = append(newCmd, fmt.Sprintf("%s", v))
newCmd[j] = fmt.Sprintf("%s", v)
}
newCommands[i] = newCmd
}

View File

@@ -135,17 +135,17 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, AlloyDBPostgresToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, AlloyDBPostgresToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddPgExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, AlloyDBPostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -168,7 +168,7 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -102,17 +102,17 @@ func TestBigQueryToolEndpoints(t *testing.T) {
)
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := getBigQueryParamToolInfo(tableNameParam)
teardownTable1 := setupBigQueryTable(t, ctx, client, createParamTableStmt, insertParamTableStmt, datasetName, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := getBigQueryParamToolInfo(tableNameParam)
teardownTable1 := setupBigQueryTable(t, ctx, client, createStatement1, insertStatement1, datasetName, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := getBigQueryAuthToolInfo(tableNameAuth)
teardownTable2 := setupBigQueryTable(t, ctx, client, createAuthTableStmt, insertAuthTableStmt, datasetName, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := getBigQueryAuthToolInfo(tableNameAuth)
teardownTable2 := setupBigQueryTable(t, ctx, client, createStatement2, insertStatement2, datasetName, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, BigqueryToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, BigqueryToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = addBigQueryPrebuiltToolsConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := getBigQueryTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, BigqueryToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -139,7 +139,7 @@ func TestBigQueryToolEndpoints(t *testing.T) {
datasetInfoWant := "\"Location\":\"US\",\"DefaultTableExpiration\":0,\"Labels\":null,\"Access\":"
tableInfoWant := "[{\"Name\":\"\",\"Location\":\"US\",\"Description\":\"\",\"Schema\":[{\"Name\":\"id\""
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
templateParamTestConfig := tests.NewTemplateParameterTestConfig(
tests.WithCreateColArray(`["id INT64", "name STRING", "age INT64"]`),
@@ -154,21 +154,20 @@ func TestBigQueryToolEndpoints(t *testing.T) {
}
// getBigQueryParamToolInfo returns statements and param for my-param-tool for bigquery kind
func getBigQueryParamToolInfo(tableName string) (string, string, string, string, string, []bigqueryapi.QueryParameter) {
func getBigQueryParamToolInfo(tableName string) (string, string, string, string, []bigqueryapi.QueryParameter) {
createStatement := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (id INT64, name STRING);`, tableName)
insertStatement := fmt.Sprintf(`
INSERT INTO %s (id, name) VALUES (?, ?), (?, ?), (?, ?), (?, NULL);`, tableName)
toolStatement := fmt.Sprintf(`SELECT * FROM %s WHERE id = ? OR name = ? ORDER BY id;`, tableName)
toolStatement2 := fmt.Sprintf(`SELECT * FROM %s WHERE id = ? ORDER BY id;`, tableName)
arrayToolStatememt := fmt.Sprintf(`SELECT * FROM %s WHERE id IN UNNEST(@idArray) AND name IN UNNEST(@nameArray) ORDER BY id;`, tableName)
params := []bigqueryapi.QueryParameter{
{Value: int64(1)}, {Value: "Alice"},
{Value: int64(2)}, {Value: "Jane"},
{Value: int64(3)}, {Value: "Sid"},
{Value: int64(4)},
}
return createStatement, insertStatement, toolStatement, toolStatement2, arrayToolStatememt, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// getBigQueryAuthToolInfo returns statements and param of my-auth-tool for bigquery kind

View File

@@ -80,10 +80,6 @@ func TestBigtableToolEndpoints(t *testing.T) {
// The structure and value of seed data has to match https://github.com/googleapis/genai-toolbox/blob/4dba0df12dc438eca3cb476ef52aa17cdf232c12/tests/common_test.go#L200-L251
paramTestStatement := fmt.Sprintf("SELECT TO_INT64(cf['id']) as id, CAST(cf['name'] AS string) as name, FROM %s WHERE TO_INT64(cf['id']) = @id OR CAST(cf['name'] AS string) = @name;", tableName)
paramTestStatement2 := fmt.Sprintf("SELECT TO_INT64(cf['id']) as id, CAST(cf['name'] AS string) as name, FROM %s WHERE TO_INT64(cf['id']) = @id;", tableName)
arrayTestStatement := fmt.Sprintf(
"SELECT TO_INT64(cf['id']) AS id, CAST(cf['name'] AS string) AS name FROM %s WHERE TO_INT64(cf['id']) IN UNNEST(@idArray) AND CAST(cf['name'] AS string) IN UNNEST(@nameArray);",
tableName,
)
teardownTable1 := setupBtTable(t, ctx, sourceConfig["project"].(string), sourceConfig["instance"].(string), tableName, columnFamilyName, muts, rowKeys)
defer teardownTable1(t)
@@ -98,7 +94,7 @@ func TestBigtableToolEndpoints(t *testing.T) {
defer teardownTableTmpl(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, BigtableToolKind, paramTestStatement, paramTestStatement2, arrayTestStatement, authToolStatement)
toolsFile := tests.GetToolsConfig(sourceConfig, BigtableToolKind, paramTestStatement, paramTestStatement2, authToolStatement)
toolsFile = addTemplateParamConfig(t, toolsFile)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
@@ -122,7 +118,7 @@ func TestBigtableToolEndpoints(t *testing.T) {
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to prepare statement: rpc error: code = InvalidArgument desc = Syntax error: Unexpected identifier \"SELEC\" [at 1:1]"}],"isError":true}}`
invokeParamWant, _, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
invokeParamWantNull := `[{"id":4,"name":""}]`
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
templateParamTestConfig := tests.NewTemplateParameterTestConfig(

View File

@@ -129,17 +129,17 @@ func TestCloudSQLMSSQLToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetMSSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, db, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMSSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, db, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMSSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, db, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMSSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, db, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMSSQLToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMSSQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMSSQLExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMSSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLMSSQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -162,7 +162,7 @@ func TestCloudSQLMSSQLToolEndpoints(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetMSSQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, false)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -116,17 +116,17 @@ func TestCloudSQLMySQLToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetMySQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMySQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMySQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMySQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMySQLToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMySQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMySqlExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLMySQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -149,7 +149,7 @@ func TestCloudSQLMySQLToolEndpoints(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetMySQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, false)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -120,17 +120,17 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLPostgresToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLPostgresToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddPgExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLPostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -153,7 +153,7 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -28,7 +28,7 @@ import (
)
// GetToolsConfig returns a mock tools config file
func GetToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, paramToolStatement2, arrayToolStatement, authToolStatement string) map[string]any {
func GetToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, paramToolStatement2, authToolStatement string) map[string]any {
// Write config into a file and pass it to command
toolsFile := map[string]any{
"sources": map[string]any{
@@ -78,34 +78,6 @@ func GetToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, p
},
},
},
"my-array-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",
"description": "Tool to test invocation with array params.",
"statement": arrayToolStatement,
"parameters": []any{
map[string]any{
"name": "idArray",
"type": "array",
"description": "ID array",
"items": map[string]any{
"name": "id",
"type": "integer",
"description": "ID",
},
},
map[string]any{
"name": "nameArray",
"type": "array",
"description": "user name array",
"items": map[string]any{
"name": "name",
"type": "string",
"description": "user name",
},
},
},
},
"my-auth-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",
@@ -302,14 +274,13 @@ func AddMSSQLExecuteSqlConfig(t *testing.T, config map[string]any) map[string]an
}
// GetPostgresSQLParamToolInfo returns statements and param for my-param-tool postgres-sql kind
func GetPostgresSQLParamToolInfo(tableName string) (string, string, string, string, string, []any) {
func GetPostgresSQLParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id SERIAL PRIMARY KEY, name TEXT);", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES ($1), ($2), ($3), ($4);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = $1 OR name = $2;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = $1;", tableName)
arrayToolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ANY($1) AND name = ANY($2);", tableName)
params := []any{"Alice", "Jane", "Sid", nil}
return createStatement, insertStatement, toolStatement, toolStatement2, arrayToolStatement, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// GetPostgresSQLAuthToolInfo returns statements and param of my-auth-tool for postgres-sql kind
@@ -329,14 +300,13 @@ func GetPostgresSQLTmplToolStatement() (string, string) {
}
// GetMSSQLParamToolInfo returns statements and param for my-param-tool mssql-sql kind
func GetMSSQLParamToolInfo(tableName string) (string, string, string, string, string, []any) {
func GetMSSQLParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(255));", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (@alice), (@jane), (@sid), (@nil);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = @id OR name = @p2;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = @id;", tableName)
arrayToolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ANY(@idArray) OR name = ANY(@p2);", tableName)
params := []any{sql.Named("alice", "Alice"), sql.Named("jane", "Jane"), sql.Named("sid", "Sid"), sql.Named("nil", nil)}
return createStatement, insertStatement, toolStatement, toolStatement2, arrayToolStatement, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// GetMSSQLAuthToolInfo returns statements and param of my-auth-tool for mssql-sql kind
@@ -356,14 +326,13 @@ func GetMSSQLTmplToolStatement() (string, string) {
}
// GetMySQLParamToolInfo returns statements and param for my-param-tool mysql-sql kind
func GetMySQLParamToolInfo(tableName string) (string, string, string, string, string, []any) {
func GetMySQLParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255));", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?), (?), (?), (?);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ? OR name = ?;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = ?;", tableName)
arrayToolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ANY(?) AND name = ANY(?);", tableName)
params := []any{"Alice", "Jane", "Sid", nil}
return createStatement, insertStatement, toolStatement, toolStatement2, arrayToolStatement, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// GetMySQLAuthToolInfo returns statements and param of my-auth-tool for mysql-sql kind
@@ -559,24 +528,6 @@ func GetRedisValkeyToolsConfig(sourceConfig map[string]any, toolKind string) map
},
},
},
"my-array-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",
"description": "Tool to test invocation with array params.",
"commands": [][]string{{"HGETALL", "row1"}, {"$cmdArray"}},
"parameters": []any{
map[string]any{
"name": "cmdArray",
"type": "array",
"description": "cmd array",
"items": map[string]any{
"name": "cmd",
"type": "string",
"description": "field",
},
},
},
},
"my-auth-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",

View File

@@ -103,13 +103,13 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
collectionNameTemplateParam := "template_param_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// Set up data for param tool
paramToolStatement, paramToolStmt2, arrayToolStatement, paramTestParams := getCouchbaseParamToolInfo(collectionNameParam)
teardownCollection1 := setupCouchbaseCollection(t, ctx, cluster, couchbaseBucket, couchbaseScope, collectionNameParam, paramTestParams)
paramToolStatement1, paramToolStatement2, params1 := getCouchbaseParamToolInfo(collectionNameParam)
teardownCollection1 := setupCouchbaseCollection(t, ctx, cluster, couchbaseBucket, couchbaseScope, collectionNameParam, params1)
defer teardownCollection1(t)
// Set up data for auth tool
authToolStatement, authTestParams := getCouchbaseAuthToolInfo(collectionNameAuth)
teardownCollection2 := setupCouchbaseCollection(t, ctx, cluster, couchbaseBucket, couchbaseScope, collectionNameAuth, authTestParams)
authToolStatement, params2 := getCouchbaseAuthToolInfo(collectionNameAuth)
teardownCollection2 := setupCouchbaseCollection(t, ctx, cluster, couchbaseBucket, couchbaseScope, collectionNameAuth, params2)
defer teardownCollection2(t)
// Setup up table for template param tool
@@ -118,7 +118,7 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
defer teardownCollection3(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, couchbaseToolKind, paramToolStatement, paramToolStmt2, arrayToolStatement, authToolStatement)
toolsFile := tests.GetToolsConfig(sourceConfig, couchbaseToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, couchbaseToolKind, tmplSelectCombined, tmplSelectFilterCombined, tmplSelectAll)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
@@ -141,7 +141,7 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
failMcpInvocationWant := "{\"jsonrpc\":\"2.0\",\"id\":\"invoke-fail-tool\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"unable to execute query: parsing failure | {\\\"statement\\\":\\\"SELEC 1;\\\""
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failMcpInvocationWant)
templateParamTestConfig := tests.NewTemplateParameterTestConfig(
@@ -231,26 +231,22 @@ func setupCouchbaseCollection(t *testing.T, ctx context.Context, cluster *gocb.C
}
// getCouchbaseParamToolInfo returns statements and params for my-param-tool couchbase-sql kind
func getCouchbaseParamToolInfo(collectionName string) (string, string, string, []map[string]any) {
func getCouchbaseParamToolInfo(collectionName string) (string, string, []map[string]any) {
// N1QL uses positional or named parameters with $ prefix
toolStatement := fmt.Sprintf("SELECT TONUMBER(meta().id) as id, "+
"%s.* FROM %s WHERE meta().id = TOSTRING($id) OR name = $name order by meta().id",
collectionName, collectionName)
toolStatement2 := fmt.Sprintf("SELECT TONUMBER(meta().id) as id, "+
"%s.* FROM %s WHERE meta().id = TOSTRING($id) order by meta().id",
collectionName, collectionName)
arrayToolStatemnt := fmt.Sprintf("SELECT TONUMBER(meta().id) as id, "+
"%s.* FROM %s WHERE TONUMBER(meta().id) IN $idArray AND name IN $nameArray order by meta().id", collectionName, collectionName)
params := []map[string]any{
{"name": "Alice"},
{"name": "Jane"},
{"name": "Sid"},
{"name": nil},
}
return toolStatement, toolStatement2, arrayToolStatemnt, params
return toolStatement, toolStatement2, params
}
// getCouchbaseAuthToolInfo returns statements and param of my-auth-tool for couchbase-sql kind

View File

@@ -287,7 +287,7 @@ func TestHttpToolEndpoints(t *testing.T) {
select1Want := `["Hello","World"]`
invokeParamWant, invokeParamWantNull, _ := tests.GetNonSpannerInvokeParamWant()
tests.RunToolGetTest(t)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, false)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
runAdvancedHTTPInvokeTest(t)
}

View File

@@ -102,17 +102,17 @@ func TestMSSQLToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetMSSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMSSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMSSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMSSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, MSSQLToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, MSSQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMSSQLExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMSSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MSSQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -135,7 +135,7 @@ func TestMSSQLToolEndpoints(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetMSSQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, false)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -93,17 +93,17 @@ func TestMySQLToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetMySQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMySQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetMySQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMySQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, MySQLToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, MySQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMySqlExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MySQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -126,7 +126,7 @@ func TestMySQLToolEndpoints(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetMySQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, false)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -99,17 +99,17 @@ func TestPostgres(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, PostgresToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, PostgresToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddPgExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, PostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -132,7 +132,7 @@ func TestPostgres(t *testing.T) {
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -100,7 +100,7 @@ func TestRedisToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetRedisValkeyWants()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
}

View File

@@ -67,7 +67,7 @@ func StartCmd(ctx context.Context, toolsFile map[string]any, args ...string) (*C
if err != nil {
return nil, nil, fmt.Errorf("unable to write tools file: %s", err)
}
args = append(args, "--tools-file", path)
args = append(args, "--tools_file", path)
ctx, cancel := context.WithCancel(ctx)
// Open a pipe for tracking the output from the cmd

View File

@@ -108,19 +108,19 @@ func TestSpannerToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := getSpannerParamToolInfo(tableNameParam)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := getSpannerParamToolInfo(tableNameParam)
dbString := fmt.Sprintf(
"projects/%s/instances/%s/databases/%s",
SpannerProject,
SpannerInstance,
SpannerDatabase,
)
teardownTable1 := setupSpannerTable(t, ctx, adminClient, dataClient, createParamTableStmt, insertParamTableStmt, tableNameParam, dbString, paramTestParams)
teardownTable1 := setupSpannerTable(t, ctx, adminClient, dataClient, createStatement1, insertStatement1, tableNameParam, dbString, params1)
defer teardownTable1(t)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := getSpannerAuthToolInfo(tableNameAuth)
teardownTable2 := setupSpannerTable(t, ctx, adminClient, dataClient, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, dbString, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := getSpannerAuthToolInfo(tableNameAuth)
teardownTable2 := setupSpannerTable(t, ctx, adminClient, dataClient, createStatement2, insertStatement2, tableNameAuth, dbString, params2)
defer teardownTable2(t)
// set up data for template param tool
@@ -129,7 +129,7 @@ func TestSpannerToolEndpoints(t *testing.T) {
defer teardownTableTmpl(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, SpannerToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, SpannerToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = addSpannerExecuteSqlConfig(t, toolsFile)
toolsFile = addSpannerReadOnlyConfig(t, toolsFile)
toolsFile = addTemplateParamConfig(t, toolsFile)
@@ -157,7 +157,7 @@ func TestSpannerToolEndpoints(t *testing.T) {
mcpInvokeParamWant := `{"jsonrpc":"2.0","id":"my-param-tool","result":{"content":[{"type":"text","text":"{\"id\":\"1\",\"name\":\"Alice\"}"},{"type":"text","text":"{\"id\":\"3\",\"name\":\"Sid\"}"}]}}`
failInvocationWant := `"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute client: unable to parse row: spanner: code = \"InvalidArgument\", desc = \"Syntax error: Unexpected identifier \\\\\\\"SELEC\\\\\\\" [at 1:1]\\\\nSELEC 1;\\\\n^\"`
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
runSpannerSchemaToolInvokeTest(t, accessSchemaWant)
runSpannerExecuteSqlToolInvokeTest(t, select1Want, invokeParamWant, tableNameParam, tableNameAuth)
@@ -171,14 +171,13 @@ func TestSpannerToolEndpoints(t *testing.T) {
}
// getSpannerToolInfo returns statements and param for my-param-tool for spanner-sql kind
func getSpannerParamToolInfo(tableName string) (string, string, string, string, string, map[string]any) {
func getSpannerParamToolInfo(tableName string) (string, string, string, string, map[string]any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT64, name STRING(MAX)) PRIMARY KEY (id)", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (id, name) VALUES (1, @name1), (2, @name2), (3, @name3), (4, @name4)", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = @id OR name = @name", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = @id", tableName)
arrayToolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id IN UNNEST(@idArray) AND name IN UNNEST(@nameArray)", tableName)
params := map[string]any{"name1": "Alice", "name2": "Jane", "name3": "Sid", "name4": nil}
return createStatement, insertStatement, toolStatement, toolStatement2, arrayToolStatement, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// getSpannerAuthToolInfo returns statements and param of my-auth-tool for spanner-sql kind

View File

@@ -81,14 +81,13 @@ func setupSQLiteTestDB(t *testing.T, ctx context.Context, db *sql.DB, createStat
}
}
func getSQLiteParamToolInfo(tableName string) (string, string, string, string, string, []any) {
func getSQLiteParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT);", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?), (?), (?), (?);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ? OR name = ?;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = ?;", tableName)
arrayToolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ANY({{.idArray}}) AND name = ANY({{.nameArray}});", tableName)
params := []any{"Alice", "Jane", "Sid", nil}
return createStatement, insertStatement, toolStatement, toolStatement2, arrayToolStatement, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
func getSQLiteAuthToolInfo(tableName string) (string, string, string, []any) {
@@ -126,15 +125,15 @@ func TestSQLiteToolEndpoint(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := getSQLiteParamToolInfo(tableNameParam)
setupSQLiteTestDB(t, ctx, db, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := getSQLiteParamToolInfo(tableNameParam)
setupSQLiteTestDB(t, ctx, db, createStatement1, insertStatement1, tableNameParam, params1)
// set up data for auth tool
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := getSQLiteAuthToolInfo(tableNameAuth)
setupSQLiteTestDB(t, ctx, db, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
createStatement2, insertStatement2, authToolStatement, params2 := getSQLiteAuthToolInfo(tableNameAuth)
setupSQLiteTestDB(t, ctx, db, createStatement2, insertStatement2, tableNameAuth, params2)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, SQLiteToolKind, paramToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
toolsFile := tests.GetToolsConfig(sourceConfig, SQLiteToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
tmplSelectCombined, tmplSelectFilterCombined := getSQLiteTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, SQLiteToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
@@ -157,7 +156,7 @@ func TestSQLiteToolEndpoint(t *testing.T) {
select1Want := "[{\"1\":1}]"
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: SQL logic error: near \"SELEC\": syntax error (1)"}],"isError":true}}`
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, false)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())
}

View File

@@ -76,7 +76,7 @@ func RunToolGetTest(t *testing.T) {
}
// RunToolInvoke runs the tool invoke endpoint
func RunToolInvokeTest(t *testing.T, select1Want, invokeParamWant, invokeParamWantNull string, supportsArray bool) {
func RunToolInvokeTest(t *testing.T, select1Want, invokeParamWant, invokeParamWantNull string) {
// Get ID token
idToken, err := GetGoogleIdToken(ClientId)
if err != nil {
@@ -130,14 +130,6 @@ func RunToolInvokeTest(t *testing.T, select1Want, invokeParamWant, invokeParamWa
requestBody: bytes.NewBuffer([]byte(`{"id": 1}`)),
isErr: true,
},
{
name: "invoke my-array-tool",
api: "http://127.0.0.1:5000/api/tool/my-array-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"idArray": [1,2,3], "nameArray": ["Alice", "Sid", "RandomName"], "cmdArray": ["HGETALL", "row3"]}`)),
want: invokeParamWant,
isErr: !supportsArray,
},
{
name: "Invoke my-auth-tool with auth token",
api: "http://127.0.0.1:5000/api/tool/my-auth-tool/invoke",

View File

@@ -1,118 +0,0 @@
// 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 utility
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"regexp"
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
func RunWaitTool(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
args := []string{"--port", "5001"}
toolsFile := map[string]any{
"tools": map[string]any{
"my-wait-for-tool": map[string]any{
"kind": "wait",
"description": "Wait for a specified duration.",
"timeout": "30s",
},
},
}
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
t.Fatalf("command initialization returned an error: %s", err)
}
defer cleanup()
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
}
invokeTcs := []struct {
name string
api string
requestBody io.Reader
want string
isErr bool
}{
{
name: "invoke my-wait-for-tool",
api: "http://127.0.0.1:5001/api/tool/my-wait-for-tool/invoke",
requestBody: bytes.NewBuffer([]byte(`{"duration": "1s"}`)),
want: `["Wait for 1s completed successfully."]`,
isErr: false,
},
{
name: "invoke my-wait-for-tool with invalid duration",
api: "http://127.0.0.1:5001/api/tool/my-wait-for-tool/invoke",
requestBody: bytes.NewBuffer([]byte(`{"duration": "invalid"}`)),
isErr: true,
},
}
for _, tc := range invokeTcs {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
if err != nil {
t.Fatalf("unable to create request: %s", err)
}
req.Header.Add("Content-type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unable to send request: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if tc.isErr {
return
}
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
}
var body map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&body)
if err != nil {
t.Fatalf("error parsing response body")
}
got, ok := body["result"].(string)
if !ok {
t.Fatalf("unable to find result in response body")
}
if got != tc.want {
t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
}
})
}
}

View File

@@ -103,7 +103,7 @@ func TestValkeyToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetRedisValkeyWants()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, true)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
}