diff --git a/.hugo/hugo.toml b/.hugo/hugo.toml
index 685f6624abca..e3b996b7cafb 100644
--- a/.hugo/hugo.toml
+++ b/.hugo/hugo.toml
@@ -51,6 +51,10 @@ ignoreFiles = ["quickstart/shared", "quickstart/python", "quickstart/js", "quick
# Add a new version block here before every release
# The order of versions in this file is mirrored into the dropdown
+[[params.versions]]
+ version = "v0.23.0"
+ url = "https://googleapis.github.io/genai-toolbox/v0.23.0/"
+
[[params.versions]]
version = "v0.22.0"
url = "https://googleapis.github.io/genai-toolbox/v0.22.0/"
diff --git a/README.md b/README.md
index 4d2369da787c..4ee0eec35e34 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,21 @@ redeploying your application.
## Getting Started
+### (Non-production) Running Toolbox
+
+You can run Toolbox directly with a [configuration file](#configuration):
+
+```sh
+npx @toolbox-sdk/server --tools-file tools.yaml
+```
+
+This runs the latest version of the toolbox server with your configuration file.
+
+> [!NOTE]
+> This method should only be used for non-production use cases such as
+> experimentation. For any production use-cases, please consider [Installing the
+> server](#installing-the-server) and then [running it](#running-the-server).
+
### Installing the server
For the latest version, check the [releases page][releases] and use the
@@ -303,6 +318,16 @@ toolbox --tools-file "tools.yaml"
+
+NPM
+
+To run Toolbox directly without manually downloading the binary (requires Node.js):
+```sh
+npx @toolbox-sdk/server --tools-file tools.yaml
+```
+
+
+
Gemini CLI
diff --git a/docs/en/getting-started/introduction/_index.md b/docs/en/getting-started/introduction/_index.md
index b346038c4ec4..ddc6bdf45504 100644
--- a/docs/en/getting-started/introduction/_index.md
+++ b/docs/en/getting-started/introduction/_index.md
@@ -71,6 +71,22 @@ redeploying your application.
## Getting Started
+### (Non-production) Running Toolbox
+
+You can run Toolbox directly with a [configuration file](../configure.md):
+
+```sh
+npx @toolbox-sdk/server --tools-file tools.yaml
+```
+
+This runs the latest version of the toolbox server with your configuration file.
+
+{{< notice note >}}
+This method should only be used for non-production use cases such as
+experimentation. For any production use-cases, please consider [Installing the
+server](#installing-the-server) and then [running it](#running-the-server).
+{{< /notice >}}
+
### Installing the server
For the latest version, check the [releases page][releases] and use the
diff --git a/docs/en/getting-started/prompts_quickstart_gemini_cli.md b/docs/en/getting-started/prompts_quickstart_gemini_cli.md
new file mode 100644
index 000000000000..2061acd7fae9
--- /dev/null
+++ b/docs/en/getting-started/prompts_quickstart_gemini_cli.md
@@ -0,0 +1,245 @@
+---
+title: "Prompts using Gemini CLI"
+type: docs
+weight: 5
+description: >
+ How to get started using Toolbox prompts locally with PostgreSQL and [Gemini CLI](https://pypi.org/project/gemini-cli/).
+---
+
+## Before you begin
+
+This guide assumes you have already done the following:
+
+1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres].
+
+[install-postgres]: https://www.postgresql.org/download/
+
+## 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.
+
+
+
+#### **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 the required tables using the following commands:
+
+ ```sql
+ CREATE TABLE users (
+ id SERIAL PRIMARY KEY,
+ username VARCHAR(50) NOT NULL,
+ email VARCHAR(100) UNIQUE NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+ );
+
+ CREATE TABLE restaurants (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ location VARCHAR(100)
+ );
+
+ CREATE TABLE reviews (
+ id SERIAL PRIMARY KEY,
+ user_id INT REFERENCES users(id),
+ restaurant_id INT REFERENCES restaurants(id),
+ rating INT CHECK (rating >= 1 AND rating <= 5),
+ review_text TEXT,
+ is_published BOOLEAN DEFAULT false,
+ moderation_status VARCHAR(50) DEFAULT 'pending_manual_review',
+ created_at TIMESTAMPTZ DEFAULT NOW()
+ );
+ ```
+
+1. Insert dummy data into the tables.
+
+ ```sql
+ INSERT INTO users (id, username, email) VALUES
+ (123, 'jane_d', 'jane.d@example.com'),
+ (124, 'john_s', 'john.s@example.com'),
+ (125, 'sam_b', 'sam.b@example.com');
+
+ INSERT INTO restaurants (id, name, location) VALUES
+ (455, 'Pizza Palace', '123 Main St'),
+ (456, 'The Corner Bistro', '456 Oak Ave'),
+ (457, 'Sushi Spot', '789 Pine Ln');
+
+ INSERT INTO reviews (user_id, restaurant_id, rating, review_text, is_published, moderation_status) VALUES
+ (124, 455, 5, 'Best pizza in town! The crust was perfect.', true, 'approved'),
+ (125, 457, 4, 'Great sushi, very fresh. A bit pricey but worth it.', true, 'approved'),
+ (123, 457, 5, 'Absolutely loved the dragon roll. Will be back!', true, 'approved'),
+ (123, 456, 4, 'The atmosphere was lovely and the food was great. My photo upload might have been weird though.', false, 'pending_manual_review'),
+ (125, 456, 1, 'This review contains inappropriate language.', false, 'rejected');
+ ```
+
+1. End the database session:
+
+ ```bash
+ \q
+ ```
+
+## Step 2: Configure Toolbox
+
+Create a file named `tools.yaml`. This file defines the database connection, the
+SQL tools available, and the prompts the agents will use.
+
+```yaml
+sources:
+ my-foodiefind-db:
+ kind: postgres
+ host: 127.0.0.1
+ port: 5432
+ database: toolbox_db
+ user: toolbox_user
+ password: my-password
+tools:
+ find_user_by_email:
+ kind: postgres-sql
+ source: my-foodiefind-db
+ description: Find a user's ID by their email address.
+ parameters:
+ - name: email
+ type: string
+ description: The email address of the user to find.
+ statement: SELECT id FROM users WHERE email = $1;
+ find_restaurant_by_name:
+ kind: postgres-sql
+ source: my-foodiefind-db
+ description: Find a restaurant's ID by its exact name.
+ parameters:
+ - name: name
+ type: string
+ description: The name of the restaurant to find.
+ statement: SELECT id FROM restaurants WHERE name = $1;
+ find_review_by_user_and_restaurant:
+ kind: postgres-sql
+ source: my-foodiefind-db
+ description: Find the full record for a specific review using the user's ID and the restaurant's ID.
+ parameters:
+ - name: user_id
+ type: integer
+ description: The numerical ID of the user.
+ - name: restaurant_id
+ type: integer
+ description: The numerical ID of the restaurant.
+ statement: SELECT * FROM reviews WHERE user_id = $1 AND restaurant_id = $2;
+prompts:
+ investigate_missing_review:
+ description: "Investigates a user's missing review by finding the user, restaurant, and the review itself, then analyzing its status."
+ arguments:
+ - name: "user_email"
+ description: "The email of the user who wrote the review."
+ - name: "restaurant_name"
+ description: "The name of the restaurant being reviewed."
+ messages:
+ - content: >-
+ **Goal:** Find the review written by the user with email '{{.user_email}}' for the restaurant named '{{.restaurant_name}}' and understand its status.
+ **Workflow:**
+ 1. Use the `find_user_by_email` tool with the email '{{.user_email}}' to get the `user_id`.
+ 2. Use the `find_restaurant_by_name` tool with the name '{{.restaurant_name}}' to get the `restaurant_id`.
+ 3. Use the `find_review_by_user_and_restaurant` tool with the `user_id` and `restaurant_id` you just found.
+ 4. Analyze the results from the final tool call. Examine the `is_published` and `moderation_status` fields and explain the review's status to the user in a clear, human-readable sentence.
+```
+
+## Step 3: Connect to Gemini CLI
+
+Configure the Gemini CLI to talk to your local Toolbox MCP server.
+
+1. Open or create your Gemini settings file: `~/.gemini/settings.json`.
+2. Add the following configuration to the file:
+
+ ```json
+ {
+ "mcpServers": {
+ "MCPToolbox": {
+ "httpUrl": "http://localhost:5000/mcp"
+ }
+ },
+ "mcp": {
+ "allowed": ["MCPToolbox"]
+ }
+ }
+ ```
+3. Start Gemini CLI using
+ ```sh
+ gemini
+ ```
+ In case Gemini CLI is already running, use `/mcp refresh` to refresh the MCP server.
+
+4. Use gemini slash commands to run your prompt:
+ ```sh
+ /investigate_missing_review --user_email="jane.d@example.com" --restaurant_name="The Corner Bistro"
+ ```
diff --git a/go.mod b/go.mod
index 09c08680772e..b2fb295beda9 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,7 @@ require (
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6
github.com/json-iterator/go v1.1.12
- github.com/looker-open-source/sdk-codegen/go v0.25.18
+ github.com/looker-open-source/sdk-codegen/go v0.25.21
github.com/microsoft/go-mssqldb v1.9.3
github.com/nakagami/firebirdsql v0.9.15
github.com/neo4j/neo4j-go-driver/v5 v5.28.4
diff --git a/go.sum b/go.sum
index ba0cf4d8ce2f..d76e60f469e3 100644
--- a/go.sum
+++ b/go.sum
@@ -1134,8 +1134,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/looker-open-source/sdk-codegen/go v0.25.18 h1:me1JBFRnOBCrDWwpoSUVDVDFcFmcYMR2ijbx6ATtwTs=
-github.com/looker-open-source/sdk-codegen/go v0.25.18/go.mod h1:Br1ntSiruDJ/4nYNjpYyWyCbqJ7+GQceWbIgn0hYims=
+github.com/looker-open-source/sdk-codegen/go v0.25.21 h1:nlZ1nz22SKluBNkzplrMHBPEVgJO3zVLF6aAws1rrRA=
+github.com/looker-open-source/sdk-codegen/go v0.25.21/go.mod h1:Br1ntSiruDJ/4nYNjpYyWyCbqJ7+GQceWbIgn0hYims=
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
diff --git a/internal/tools/clickhouse/clickhouselisttables/clickhouselisttables.go b/internal/tools/clickhouse/clickhouselisttables/clickhouselisttables.go
index 094fb28b02ed..16a3b4591135 100644
--- a/internal/tools/clickhouse/clickhouselisttables/clickhouselisttables.go
+++ b/internal/tools/clickhouse/clickhouselisttables/clickhouselisttables.go
@@ -121,7 +121,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
defer results.Close()
- var tables []map[string]any
+ tables := []map[string]any{}
for results.Next() {
var tableName string
err := results.Scan(&tableName)
diff --git a/internal/tools/mssql/mssqllisttables/mssqllisttables.go b/internal/tools/mssql/mssqllisttables/mssqllisttables.go
index 4bb186db4345..03341132e223 100644
--- a/internal/tools/mssql/mssqllisttables/mssqllisttables.go
+++ b/internal/tools/mssql/mssqllisttables/mssqllisttables.go
@@ -391,7 +391,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
values[i] = &rawValues[i]
}
- var out []any
+ out := []any{}
for rows.Next() {
err = rows.Scan(values...)
if err != nil {
diff --git a/internal/tools/mysql/mysqllisttables/mysqllisttables.go b/internal/tools/mysql/mysqllisttables/mysqllisttables.go
index e2b21aedaf20..ef4c9e666685 100644
--- a/internal/tools/mysql/mysqllisttables/mysqllisttables.go
+++ b/internal/tools/mysql/mysqllisttables/mysqllisttables.go
@@ -300,7 +300,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
return nil, fmt.Errorf("unable to get column types: %w", err)
}
- var out []any
+ out := []any{}
for results.Next() {
err := results.Scan(values...)
if err != nil {
diff --git a/internal/tools/postgres/postgreslisttables/postgreslisttables.go b/internal/tools/postgres/postgreslisttables/postgreslisttables.go
index 85b6ed5f77dc..5e949a755ec2 100644
--- a/internal/tools/postgres/postgreslisttables/postgreslisttables.go
+++ b/internal/tools/postgres/postgreslisttables/postgreslisttables.go
@@ -210,7 +210,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
defer results.Close()
fields := results.FieldDescriptions()
- var out []map[string]any
+ out := []map[string]any{}
for results.Next() {
values, err := results.Values()
diff --git a/internal/tools/spanner/spannerlisttables/spannerlisttables.go b/internal/tools/spanner/spannerlisttables/spannerlisttables.go
index 3238d885b565..b5d361ea1289 100644
--- a/internal/tools/spanner/spannerlisttables/spannerlisttables.go
+++ b/internal/tools/spanner/spannerlisttables/spannerlisttables.go
@@ -129,7 +129,7 @@ type Tool struct {
// processRows iterates over the spanner.RowIterator and converts each row to a map[string]any.
func processRows(iter *spanner.RowIterator) ([]any, error) {
- var out []any
+ out := []any{}
defer iter.Stop()
for {
diff --git a/tests/mariadb/mariadb_integration_test.go b/tests/mariadb/mariadb_integration_test.go
index 7f13e36206ba..60d734ace73e 100644
--- a/tests/mariadb/mariadb_integration_test.go
+++ b/tests/mariadb/mariadb_integration_test.go
@@ -250,7 +250,7 @@ func RunMariDBListTablesTest(t *testing.T, databaseName, tableNameParam, tableNa
name: "invoke list_tables with non-existent table",
requestBody: bytes.NewBufferString(`{"table_names": "non_existent_table"}`),
wantStatusCode: http.StatusOK,
- want: nil,
+ want: []objectDetails{},
},
}
for _, tc := range invokeTcs {
@@ -282,7 +282,7 @@ func RunMariDBListTablesTest(t *testing.T, databaseName, tableNameParam, tableNa
if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
}
- var details []map[string]any
+ details := []map[string]any{}
for _, table := range tables {
var d map[string]any
if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
@@ -292,23 +292,19 @@ func RunMariDBListTablesTest(t *testing.T, databaseName, tableNameParam, tableNa
}
got = details
} else {
- if resultString == "null" {
- got = nil
- } else {
- var tables []tableInfo
- if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
- t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
- }
- var details []objectDetails
- for _, table := range tables {
- var d objectDetails
- if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
- t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
- }
- details = append(details, d)
+ var tables []tableInfo
+ if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
+ t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
+ }
+ details := []objectDetails{}
+ for _, table := range tables {
+ var d objectDetails
+ if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
+ t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
}
- got = details
+ details = append(details, d)
}
+ got = details
}
opts := []cmp.Option{
@@ -319,7 +315,7 @@ func RunMariDBListTablesTest(t *testing.T, databaseName, tableNameParam, tableNa
// Checking only the current database where the test tables are created to avoid brittle tests.
if tc.isAllTables {
- var filteredGot []objectDetails
+ filteredGot := []objectDetails{}
if got != nil {
for _, item := range got.([]objectDetails) {
if item.SchemaName == databaseName {
@@ -327,11 +323,7 @@ func RunMariDBListTablesTest(t *testing.T, databaseName, tableNameParam, tableNa
}
}
}
- if len(filteredGot) == 0 {
- got = nil
- } else {
- got = filteredGot
- }
+ got = filteredGot
}
if diff := cmp.Diff(tc.want, got, opts...); diff != "" {
diff --git a/tests/tool.go b/tests/tool.go
index c31e404645f8..9fcd045d766f 100644
--- a/tests/tool.go
+++ b/tests/tool.go
@@ -1189,7 +1189,7 @@ func RunPostgresListTablesTest(t *testing.T, tableNameParam, tableNameAuth, user
api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
requestBody: bytes.NewBuffer([]byte(`{"table_names": "non_existent_table"}`)),
wantStatusCode: http.StatusOK,
- want: `null`,
+ want: `[]`,
},
{
name: "invoke list_tables with one existing and one non-existent table",
@@ -2822,7 +2822,7 @@ func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNam
name: "invoke list_tables with non-existent table",
requestBody: bytes.NewBufferString(`{"table_names": "non_existent_table"}`),
wantStatusCode: http.StatusOK,
- want: nil,
+ want: []objectDetails{},
},
}
for _, tc := range invokeTcs {
@@ -2854,7 +2854,7 @@ func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNam
if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
}
- var details []map[string]any
+ details := []map[string]any{}
for _, table := range tables {
var d map[string]any
if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
@@ -2864,23 +2864,19 @@ func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNam
}
got = details
} else {
- if resultString == "null" {
- got = nil
- } else {
- var tables []tableInfo
- if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
- t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
- }
- var details []objectDetails
- for _, table := range tables {
- var d objectDetails
- if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
- t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
- }
- details = append(details, d)
+ var tables []tableInfo
+ if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
+ t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
+ }
+ details := []objectDetails{}
+ for _, table := range tables {
+ var d objectDetails
+ if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
+ t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
}
- got = details
+ details = append(details, d)
}
+ got = details
}
opts := []cmp.Option{
@@ -2891,7 +2887,7 @@ func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNam
// Checking only the current database where the test tables are created to avoid brittle tests.
if tc.isAllTables {
- var filteredGot []objectDetails
+ filteredGot := []objectDetails{}
if got != nil {
for _, item := range got.([]objectDetails) {
if item.SchemaName == databaseName {
@@ -2899,11 +2895,7 @@ func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNam
}
}
}
- if len(filteredGot) == 0 {
- got = nil
- } else {
- got = filteredGot
- }
+ got = filteredGot
}
if diff := cmp.Diff(tc.want, got, opts...); diff != "" {
@@ -3491,7 +3483,7 @@ func RunMSSQLListTablesTest(t *testing.T, tableNameParam, tableNameAuth string)
api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
requestBody: `{"table_names": "non_existent_table"}`,
wantStatusCode: http.StatusOK,
- want: `null`,
+ want: `[]`,
},
{
name: "invoke list_tables with one existing and one non-existent table",