mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-12 08:58:28 -05:00
Compare commits
9 Commits
envvariabl
...
map
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0f94a559b | ||
|
|
32dbb0c0f4 | ||
|
|
ce2c121c95 | ||
|
|
2b2732ec39 | ||
|
|
9b1505e4bd | ||
|
|
b7795c8857 | ||
|
|
000831c15b | ||
|
|
5c54cc973d | ||
|
|
8cc91ee3f7 |
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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 |
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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
4
go.mod
@@ -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
8
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user