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
13 changed files with 581 additions and 117 deletions

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

@@ -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)
}
})
}