Compare commits

..

9 Commits

Author SHA1 Message Date
duwenxin
e0f94a559b modify object parsing 2025-07-17 15:23:02 -04:00
duwenxin
32dbb0c0f4 add docs 2025-07-17 13:39:53 -04:00
duwenxin
ce2c121c95 feat: Add support for Object parameters` 2025-07-17 13:39:53 -04:00
Harsh Jha
2b2732ec39 docs: fix valkey doc broken link (#916)
In
[genai-toolbox](https://googleapis.github.io/genai-toolbox/resources/sources/valkey/)
Official Documentation of valkey URL is broken.

---------

Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
2025-07-17 06:23:19 +00:00
Mend Renovate
9b1505e4bd chore(deps): update module google.golang.org/api to v0.242.0 (#913)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[google.golang.org/api](https://redirect.github.com/googleapis/google-api-go-client)
| `v0.241.0` -> `v0.242.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fapi/v0.242.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fapi/v0.241.0/v0.242.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>googleapis/google-api-go-client
(google.golang.org/api)</summary>

###
[`v0.242.0`](https://redirect.github.com/googleapis/google-api-go-client/releases/tag/v0.242.0)

[Compare
Source](https://redirect.github.com/googleapis/google-api-go-client/compare/v0.241.0...v0.242.0)

##### Features

- **all:** Auto-regenerate discovery clients
([#&#8203;3226](https://redirect.github.com/googleapis/google-api-go-client/issues/3226))
([9bd47c4](9bd47c484b))
- **all:** Auto-regenerate discovery clients
([#&#8203;3228](https://redirect.github.com/googleapis/google-api-go-client/issues/3228))
([2ee2e31](2ee2e31870))
- **all:** Auto-regenerate discovery clients
([#&#8203;3229](https://redirect.github.com/googleapis/google-api-go-client/issues/3229))
([6fdc3eb](6fdc3ebb20))
- **all:** Auto-regenerate discovery clients
([#&#8203;3230](https://redirect.github.com/googleapis/google-api-go-client/issues/3230))
([d5fa61e](d5fa61e954))
- **all:** Auto-regenerate discovery clients
([#&#8203;3231](https://redirect.github.com/googleapis/google-api-go-client/issues/3231))
([96d4d98](96d4d98a3d))
- **all:** Auto-regenerate discovery clients
([#&#8203;3232](https://redirect.github.com/googleapis/google-api-go-client/issues/3232))
([2ab275b](2ab275bbcb))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2025-07-17 01:11:11 +00:00
Mend Renovate
b7795c8857 chore(deps): update module github.com/valkey-io/valkey-go to v1.0.63 (#907)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[github.com/valkey-io/valkey-go](https://redirect.github.com/valkey-io/valkey-go)
| `v1.0.62` -> `v1.0.63` |
[![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fvalkey-io%2fvalkey-go/v1.0.63?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fvalkey-io%2fvalkey-go/v1.0.62/v1.0.63?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>valkey-io/valkey-go (github.com/valkey-io/valkey-go)</summary>

###
[`v1.0.63`](https://redirect.github.com/valkey-io/valkey-go/releases/tag/v1.0.63):
1.0.63

[Compare
Source](https://redirect.github.com/valkey-io/valkey-go/compare/v1.0.62...v1.0.63)

### Changes

- feat: Add XDELEX command
- feat: add XACKDEL command
- feat: add KEEPREF, DELREF, and ACKED options to XTRIM command
- feat: Add KEEPREF, DELREF, and ACKED options to XADD command
- feat: add WITHATTRIBS option to command VSIM
- feat: add DIFF, DIFF1, ANDOR, and ONE options to BITOP command
- feat: add HStrLen to valkeycompat
- feat: Add TotalNetIn, TotalNetOut, and TotalCmds fields to
valkeycompat.ClientInfo
- feat: add Scanner implementation with Iter and Iter2 methods for XSCAN
- feat: allow non-blocking client initialization when ForceSingleClient
is set
- perf: replace json.NewDecoder with json.Unmarshal
- perf: reduce mux size by consolidating wire, sc, mu into one struct
- perf: allocate fields for RESP2 PubSub only when necessary

#### Contributors

We'd like to thank all the contributors who worked on this release!

[@&#8203;Aakkash-Suresh](https://redirect.github.com/Aakkash-Suresh),
[@&#8203;Ryan2327](https://redirect.github.com/Ryan2327),
[@&#8203;arbhalerao](https://redirect.github.com/arbhalerao),
[@&#8203;ash2k](https://redirect.github.com/ash2k),
[@&#8203;dalaoqi](https://redirect.github.com/dalaoqi),
[@&#8203;davidlin-tv2](https://redirect.github.com/davidlin-tv2),
[@&#8203;mingdaoy](https://redirect.github.com/mingdaoy),
[@&#8203;rueian](https://redirect.github.com/rueian),
[@&#8203;sugymt](https://redirect.github.com/sugymt) and
[@&#8203;yhc9311](https://redirect.github.com/yhc9311)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2025-07-17 00:46:05 +00:00
Yuan Teoh
000831c15b docs: update transport protocol in mcp instructions (#893)
Current default transport protocol shown in docs is using `sse`.
Updating to use `Streamable HTTP`.

Fixes #876
2025-07-16 23:36:59 +00:00
Yuan Teoh
5c54cc973d chore(tools/http): replace nil query parameter with empty string (#892)
Replace `nil` values with empty string for query parameter.
2025-07-16 22:26:53 +00:00
Wenxin Du
8cc91ee3f7 fix: Redis and Valkey docs (#912)
Add port and make sure address is an array.

fix: https://github.com/googleapis/genai-toolbox/issues/883
2025-07-16 18:19:33 -04:00
18 changed files with 581 additions and 646 deletions

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/envvariable"
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/wait"
_ "github.com/googleapis/genai-toolbox/internal/tools/valkey"

View File

@@ -218,7 +218,7 @@ 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 (please take note of `<YOUR_SESSION_TOKEN>`):
```bash
Starting MCP inspector...
@@ -232,11 +232,11 @@ In this section, we will download Toolbox, configure our tools in a
1. Open the above link in your browser.
1. For `Transport Type`, select `SSE`.
1. For `Transport Type`, select `Streamable HTTP`.
1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse`.
1. For `URL`, type in `http://127.0.0.1:5000/mcp`.
1. For `Authentication` -> `Proxy Session Token`, make sure <YOUR_SESSION_TOKEN> is present.
1. For `Configuration` -> `Proxy Session Token`, make sure `<YOUR_SESSION_TOKEN>` is present.
1. Click Connect.
@@ -246,4 +246,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: 22 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -69,7 +69,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" lang="en" %}}
{{< tabpane text=true >}} {{% tab header="HTTP with SSE (deprecated)" lang="en" %}}
Add the following configuration to your MCP client configuration:
```bash
@@ -130,7 +130,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" lang="en" %}}
{{% tab header="HTTP with SSE (deprecated)" lang="en" %}}
1. [Run Toolbox](../getting-started/introduction/_index.md#running-the-server).
1. In a separate terminal, run Inspector directly through `npx`:

View File

@@ -33,7 +33,7 @@ sources:
my-redis-instance:
kind: redis
address:
- 127.0.0.1
- 127.0.0.1:6379
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
- 127.0.0.1:6379
password: ${MY_AUTH_STRING}
# useGCPIAM: false
# clusterEnabled: false
@@ -74,7 +74,8 @@ using IAM authentication:
sources:
my-redis-cluster-instance:
kind: memorystore-redis
address: 127.0.0.1
address:
- 127.0.0.1:6379
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/docs/getting-started/).
the [official Valkey website](https://valkey.io/topics/quickstart/).
## Example
@@ -26,7 +26,7 @@ sources:
my-valkey-instance:
kind: valkey
address:
- 127.0.0.1
- 127.0.0.1:6379
username: ${YOUR_USERNAME}
password: ${YOUR_PASSWORD}
# database: 0
@@ -50,7 +50,7 @@ sources:
my-valkey-instance:
kind: valkey
address:
- 127.0.0.1
- 127.0.0.1:6379
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,6 +115,43 @@ 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
@@ -143,10 +180,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
@@ -195,12 +232,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

@@ -1,22 +0,0 @@
# update-mcp-settings
## Description
The `update-mcp-settings` tool is a utility that updates the MCP (Model Context Protocol) settings file with the necessary environment variables for a given tool. This is particularly useful when you need to configure a tool with specific environment variables being set previously in chat for AlloyDB Control Plane.
## Configuration
To use the `update-mcp-settings` tool, you need to configure it in your `toolbox.yaml` file. Here is an example configuration:
```yaml
tools:
update-mcp-settings-tool:
kind: update-mcp-settings
description: "Run this tool to update mcp json file prebuilt tool for data plane with right parameters ALLOYDB_POSTGRES_PROJECT, ALLOYDB_POSTGRES_REGION, ALLOYDB_POSTGRES_CLUSTER, ALLOYDB_POSTGRES_INSTANCE, ALLOYDB_POSTGRES_DATABASE, ALLOYDB_POSTGRES_USER, ALLOYDB_POSTGRES_PASSWORD. Identify the mcp settings json file or ask user to share it's full path. Run this tool once cluster and instance creation is done."
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "update-mcp-settings". |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -208,17 +208,25 @@ 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:
1. It should show the following when the MCP Inspector is up and running (please take note of `<YOUR_SESSION_TOKEN>`):
```bash
🔍 MCP Inspector is up and running at http://127.0.0.1:5173 🚀
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>
```
1. Open the above link in your browser.
1. For `Transport Type`, select `SSE`.
1. For `Transport Type`, select `Streamable HTTP`.
1. For `URL`, type in `http://127.0.0.1:5000/mcp/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. Click Connect.
@@ -228,4 +236,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: 22 KiB

After

Width:  |  Height:  |  Size: 32 KiB

4
go.mod
View File

@@ -29,7 +29,7 @@ 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.62
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
@@ -39,7 +39,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/oauth2 v0.30.0
google.golang.org/api v0.241.0
google.golang.org/api v0.242.0
modernc.org/sqlite v1.38.0
)

8
go.sum
View File

@@ -1094,8 +1094,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.62 h1:oQdPlQGRyxcQWL8fnu6J3SCaQwayc/hRZifjJIaJqu0=
github.com/valkey-io/valkey-go v1.0.62/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
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/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=
@@ -1607,8 +1607,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.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE=
google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
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/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=

View File

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

View File

@@ -32,6 +32,7 @@ const (
typeFloat = "float"
typeBool = "boolean"
typeArray = "array"
typeObject = "object"
)
// ParamValues is an ordered list of ParamValue
@@ -367,6 +368,17 @@ 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)
}
@@ -401,19 +413,23 @@ 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"`
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"`
}
// 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"`
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"`
}
// CommonParameter are default fields that are emebdding in most Parameter implementations. Embedding this stuct will give the object Name() and Type() functions.
@@ -1022,3 +1038,194 @@ 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,6 +23,7 @@ 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"
)
@@ -200,6 +201,31 @@ 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{
@@ -294,6 +320,37 @@ 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) {
@@ -350,13 +407,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "string with authSources",
name: "string with authServices",
in: []map[string]any{
{
"name": "my_string",
"type": "string",
"description": "this param is a string",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -396,13 +453,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "int with authSources",
name: "int with authServices",
in: []map[string]any{
{
"name": "my_integer",
"type": "integer",
"description": "this param is an int",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -442,13 +499,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "float with authSources",
name: "float with authServices",
in: []map[string]any{
{
"name": "my_float",
"type": "float",
"description": "my param is a float",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -488,13 +545,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "bool with authSources",
name: "bool with authServices",
in: []map[string]any{
{
"name": "my_bool",
"type": "boolean",
"description": "this param is a boolean",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -539,7 +596,7 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "string array with authSources",
name: "string array with authServices",
in: []map[string]any{
{
"name": "my_array",
@@ -550,7 +607,7 @@ func TestAuthParametersMarshal(t *testing.T) {
"type": "string",
"description": "string item",
},
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -594,6 +651,46 @@ 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) {
@@ -617,10 +714,11 @@ func TestAuthParametersMarshal(t *testing.T) {
func TestParametersParse(t *testing.T) {
tcs := []struct {
name string
params tools.Parameters
in map[string]any
want tools.ParamValues
name string
params tools.Parameters
in map[string]any
want tools.ParamValues
wantErr bool
}{
{
name: "string",
@@ -640,6 +738,7 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_string": 4,
},
wantErr: true,
},
{
name: "int",
@@ -659,16 +758,17 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_int": 14.5,
},
wantErr: true,
},
{
name: "not int (big)",
name: "int from json.Number",
params: tools.Parameters{
tools.NewIntParameter("my_int", "this param is an int"),
},
in: map[string]any{
"my_int": math.MaxInt64,
"my_int": json.Number("9223372036854775807"),
},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: math.MaxInt64}},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: int(math.MaxInt64)}},
},
{
name: "float",
@@ -688,6 +788,7 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_float": true,
},
wantErr: true,
},
{
name: "bool",
@@ -707,6 +808,7 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{
"my_bool": 1.5,
},
wantErr: true,
},
{
name: "string default",
@@ -780,44 +882,103 @@ func TestParametersParse(t *testing.T) {
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) {
// 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
got, err := tools.ParseParams(tc.params, tc.in, make(map[string]map[string]any))
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
if tc.wantErr {
if err == nil {
t.Fatal("expected error but got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error from ParseParams: %s", err)
}
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)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("ParseParams() mismatch (-want +got):\n%s", diff)
}
})
}
@@ -1075,7 +1236,27 @@ 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{}},
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{}},
},
},
{
@@ -1142,12 +1323,25 @@ func TestParamManifest(t *testing.T) {
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 !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
}
})
}
@@ -1188,12 +1382,22 @@ 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 !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
}
})
}
@@ -1206,7 +1410,7 @@ func TestMcpManifest(t *testing.T) {
want tools.McpToolsSchema
}{
{
name: "string",
name: "various parameters",
in: tools.Parameters{
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
tools.NewStringParameter("foo-string2", "bar"),
@@ -1214,38 +1418,41 @@ func TestMcpManifest(t *testing.T) {
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", "bar")),
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "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),
},
want: tools.McpToolsSchema{
Type: "object",
Properties: map[string]tools.ParameterMcpManifest{
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string-req": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string-not-req": 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{
"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": {
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
"foo-array2": tools.ParameterMcpManifest{
"foo-array2": {
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-string2", "foo-string-req", "foo-int2", "foo-array2"},
Required: []string{"foo-array2", "foo-int2", "foo-object", "foo-string-req", "foo-string2"},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.McpManifest()
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
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)
}
})
}

View File

@@ -1,209 +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 setenvvariable
import (
"context"
"encoding/json"
"fmt"
"os"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
)
const kind string = "update-mcp-settings"
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"`
AuthRequired []string `yaml:"authRequired"`
}
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
projectIDParam := tools.NewStringParameter("ALLOYDB_POSTGRES_PROJECT", "The Google Cloud project ID.")
regionParam := tools.NewStringParameter("ALLOYDB_POSTGRES_REGION", "The region for AlloyDB.")
clusterParam := tools.NewStringParameter("ALLOYDB_POSTGRES_CLUSTER", "The AlloyDB cluster name.")
instanceParam := tools.NewStringParameter("ALLOYDB_POSTGRES_INSTANCE", "The AlloyDB instance name.")
databaseParam := tools.NewStringParameter("ALLOYDB_POSTGRES_DATABASE", "The AlloyDB database name (defaults to 'postgres').")
userParam := tools.NewStringParameter("ALLOYDB_POSTGRES_USER", "The database username.")
passwordParam := tools.NewStringParameter("ALLOYDB_POSTGRES_PASSWORD", "The database password.")
mcpSettingsFile := tools.NewStringParameter("mcpSettingsFile", "The MCP Settings json file which contains information about server to run for the IDE")
parameters := tools.Parameters{
projectIDParam,
regionParam,
clusterParam,
instanceParam,
databaseParam,
userParam,
passwordParam,
mcpSettingsFile,
}
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()
mcpSettingsFile, ok := paramsMap["mcpSettingsFile"]
if !ok {
return nil, fmt.Errorf("mcpSettingsFile not found in params")
}
mcpSettingsFileStr, ok := mcpSettingsFile.(string)
if !ok {
return nil, fmt.Errorf("mcpSettingsFile is not a string")
}
data, err := os.ReadFile(mcpSettingsFileStr)
if err != nil {
return nil, fmt.Errorf("failed to read mcp settings file: %w", err)
}
var mcpSettings map[string]interface{}
if err := json.Unmarshal(data, &mcpSettings); err != nil {
return nil, fmt.Errorf("failed to unmarshal mcp settings file: %w", err)
}
mcpServers, ok := mcpSettings["mcpServers"].(map[string]interface{})
if !ok {
if servers, found := mcpSettings["servers"].(map[string]interface{}); found {
mcpServers = servers
} else {
mcpServers = make(map[string]interface{})
mcpSettings["mcpServers"] = mcpServers
}
}
var targetServer map[string]interface{}
var targetServerName string
for serverName, server := range mcpServers {
serverMap, ok := server.(map[string]interface{})
if !ok {
continue
}
args, ok := serverMap["args"].([]interface{})
if !ok {
continue
}
for _, arg := range args {
if argStr, ok := arg.(string); ok && argStr == "alloydb-postgres" {
targetServer = serverMap
targetServerName = serverName
break
}
}
if targetServer != nil {
break
}
}
if targetServer == nil {
targetServerName = "alloydb"
targetServer = make(map[string]interface{})
targetServer["args"] = []interface{}{"--prebuilt", "alloydb-postgres", "--stdio"}
mcpServers[targetServerName] = targetServer
}
if _, ok := targetServer["command"]; !ok {
targetServer["command"] = "./PATH/TO/toolbox"
}
env, ok := targetServer["env"].(map[string]interface{})
if !ok {
env = make(map[string]interface{})
targetServer["env"] = env
}
for key, value := range paramsMap {
if key != "mcpSettingsFile" {
env[key] = value
}
}
updatedData, err := json.MarshalIndent(mcpSettings, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal mcp settings file: %w", err)
}
if err := os.WriteFile(mcpSettingsFileStr, updatedData, 0644); err != nil {
return nil, fmt.Errorf("failed to write mcp settings file: %w", err)
}
return []any{"Successfully updated MCP settings file"}, 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,72 +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 setenvvariable_test
import (
"testing"
"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"
setenvvariable "github.com/googleapis/genai-toolbox/internal/tools/utility/envvariable"
)
func TestParseFromYamlSetEnvVariable(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: update-mcp-settings
description: some description
authRequired:
- my-google-auth-service
`,
want: server.ToolConfigs{
"example_tool": setenvvariable.Config{
Name: "example_tool",
Kind: "update-mcp-settings",
Description: "some description",
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

@@ -1,225 +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_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/envvariable"
"github.com/googleapis/genai-toolbox/tests"
)
func TestUpdateMCPSettings(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
toolName := "my-update-mcp-settings"
mcpSettings := map[string]interface{}{
"mcpServers": map[string]interface{}{},
}
mcpSettingsData, err := json.Marshal(mcpSettings)
if err != nil {
t.Fatalf("failed to marshal mcp settings: %v", err)
}
tmpDir := t.TempDir()
mcpSettingsFile := filepath.Join(tmpDir, "mcp.json")
if err := os.WriteFile(mcpSettingsFile, mcpSettingsData, 0644); err != nil {
t.Fatalf("failed to write mcp settings file: %v", err)
}
toolsFile := map[string]any{
"tools": map[string]any{
toolName: map[string]any{
"kind": "update-mcp-settings",
"description": "Update MCP settings.",
},
},
}
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile)
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)
}
t.Run("success", func(t *testing.T) {
params := map[string]interface{}{
"mcpSettingsFile": mcpSettingsFile,
"ALLOYDB_POSTGRES_PROJECT": "my-project",
"ALLOYDB_POSTGRES_REGION": "my-region",
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
"ALLOYDB_POSTGRES_DATABASE": "my-db",
"ALLOYDB_POSTGRES_USER": "my-user",
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
}
var result struct{ Result string }
if err := invoke(toolName, params, &result); err != nil {
t.Fatalf("tool invocation failed: %v", err)
}
expectedResult := "[\"Successfully updated MCP settings file\"]"
if !reflect.DeepEqual(result.Result, expectedResult) {
t.Errorf("unexpected result: got %q, want %q", result.Result, expectedResult)
}
data, err := os.ReadFile(mcpSettingsFile)
if err != nil {
t.Fatalf("failed to read mcp settings file: %v", err)
}
var updatedMCPSettings map[string]interface{}
if err := json.Unmarshal(data, &updatedMCPSettings); err != nil {
t.Fatalf("failed to unmarshal mcp settings file: %v", err)
}
mcpServers, ok := updatedMCPSettings["mcpServers"].(map[string]interface{})
if !ok {
t.Fatalf("mcpServers not found in updated settings")
}
alloydbServer, ok := mcpServers["alloydb"].(map[string]interface{})
if !ok {
t.Fatalf("alloydb server not found in updated settings")
}
env, ok := alloydbServer["env"].(map[string]interface{})
if !ok {
t.Fatalf("env not found in alloydb server settings")
}
expectedEnv := map[string]interface{}{
"ALLOYDB_POSTGRES_PROJECT": "my-project",
"ALLOYDB_POSTGRES_REGION": "my-region",
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
"ALLOYDB_POSTGRES_DATABASE": "my-db",
"ALLOYDB_POSTGRES_USER": "my-user",
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
}
if !reflect.DeepEqual(env, expectedEnv) {
t.Errorf("unexpected env: got %v, want %v", env, expectedEnv)
}
})
t.Run("file not found", func(t *testing.T) {
params := map[string]interface{}{
"mcpSettingsFile": "non-existent-file.json",
"ALLOYDB_POSTGRES_PROJECT": "my-project",
"ALLOYDB_POSTGRES_REGION": "my-region",
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
"ALLOYDB_POSTGRES_DATABASE": "my-db",
"ALLOYDB_POSTGRES_USER": "my-user",
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
}
var result struct{ Result string }
err := invoke(toolName, params, &result)
if err == nil {
t.Fatal("expected an error but got none")
}
expectedError := "failed to read mcp settings file"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error: got %v, want to contain %v", err, expectedError)
}
})
t.Run("invalid json", func(t *testing.T) {
invalidJSONFile := filepath.Join(tmpDir, "invalid.json")
if err := os.WriteFile(invalidJSONFile, []byte("{"), 0644); err != nil {
t.Fatalf("failed to write invalid json file: %v", err)
}
params := map[string]interface{}{
"mcpSettingsFile": invalidJSONFile,
"ALLOYDB_POSTGRES_PROJECT": "my-project",
"ALLOYDB_POSTGRES_REGION": "my-region",
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
"ALLOYDB_POSTGRES_DATABASE": "my-db",
"ALLOYDB_POSTGRES_USER": "my-user",
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
}
var result struct{ Result string }
err := invoke(toolName, params, &result)
if err == nil {
t.Fatal("expected an error but got none")
}
expectedError := "failed to unmarshal mcp settings file"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error: got %v, want to contain %v", err, expectedError)
}
})
t.Run("missing mcpSettingsFile parameter", func(t *testing.T) {
params := map[string]interface{}{
"ALLOYDB_POSTGRES_PROJECT": "my-project",
"ALLOYDB_POSTGRES_REGION": "my-region",
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
"ALLOYDB_POSTGRES_DATABASE": "my-db",
"ALLOYDB_POSTGRES_USER": "my-user",
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
}
var result struct{ Result string }
err := invoke(toolName, params, &result)
if err == nil {
t.Fatal("expected an error but got none")
}
expectedError := "parameter \\\"mcpSettingsFile\\\" is required"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error: got %v, want to contain %v", err, expectedError)
}
})
}
func invoke(toolName string, params map[string]interface{}, result interface{}) error {
url := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", toolName)
body, err := json.Marshal(params)
if err != nil {
return err
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(respBody))
}
return json.NewDecoder(resp.Body).Decode(result)
}