feat: add support for optional parameters (#617)

Add a `default` field to parameters, that enables users to specify a
default value.

e.g.

```
  parameters:
    - name: name
      type: string
      default: "some-default-value"
      description: The name of the hotel.
```
if this parameter is invoked without specifying `name`, the parameter
would default to "some-default-value"


For parameter manifest, there will be an additional `Required` field.
The default `Required` field is true. If a `default` value is presented,
`Required: false`. Array parameter's item's `Required` field will
inherit the array's `Required` field.

Fixes #475
This commit is contained in:
Yuan
2025-06-20 10:46:59 -07:00
committed by GitHub
parent a8df414b11
commit 4827771b78
4 changed files with 340 additions and 29 deletions

View File

@@ -79,11 +79,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" |
| 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
@@ -102,12 +103,17 @@ in the list using the items field:
description: Name of the airline.
```
| **field** | **type** | **required** | **description** |
|-------------|:----------------:|:------------:|----------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be "array" |
| 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. |
| **field** | **type** | **required** | **description** |
|-------------|:----------------:|:------------:|-----------------------------------------------------------------------------|
| 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. |
| 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. |
{{< notice note >}}
Items in array should not have a default value. If provided, it will be ignored.
{{< /notice >}}
### Authenticated Parameters

View File

@@ -130,7 +130,10 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
var ok bool
v, ok = data[name]
if !ok {
return nil, fmt.Errorf("parameter %q is required", name)
v = p.GetDefault()
if v == nil {
return nil, fmt.Errorf("parameter %q is required", name)
}
}
} else {
// parse authenticated parameter
@@ -253,6 +256,7 @@ type Parameter interface {
// but this is done to differentiate it from the fields in CommonParameter.
GetName() string
GetType() string
GetDefault() any
GetAuthServices() []ParamAuthService
Parse(any) (any, error)
Manifest() ParameterManifest
@@ -382,8 +386,10 @@ func (ps Parameters) McpManifest() McpToolsSchema {
for _, p := range ps {
name := p.GetName()
properties[name] = p.McpManifest()
// all parameters are added to the required field
required = append(required, name)
// parameters that doesn't have a default value are added to the required field
if p.GetDefault() == nil {
required = append(required, name)
}
}
return McpToolsSchema{
@@ -397,6 +403,7 @@ func (ps Parameters) McpManifest() McpToolsSchema {
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"`
@@ -413,6 +420,7 @@ type ParameterMcpManifest struct {
type CommonParameter struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Default any `yaml:"default"`
Desc string `yaml:"description" validate:"required"`
AuthServices []ParamAuthService `yaml:"authServices"`
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
@@ -428,6 +436,10 @@ func (p *CommonParameter) GetType() string {
return p.Type
}
func (p *CommonParameter) GetDefault() any {
return p.Default
}
// Manifest returns the manifest for the Parameter.
func (p *CommonParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
@@ -435,9 +447,14 @@ func (p *CommonParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
var required bool
if p.Default == nil {
required = true
}
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
@@ -468,7 +485,7 @@ type ParamAuthService struct {
}
// NewStringParameter is a convenience function for initializing a StringParameter.
func NewStringParameter(name, desc string) *StringParameter {
func NewStringParameter(name string, desc string) *StringParameter {
return &StringParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -479,8 +496,21 @@ func NewStringParameter(name, desc string) *StringParameter {
}
}
// NewStringParameterWithDefault is a convenience function for initializing a StringParameter with default value.
func NewStringParameterWithDefault(name string, defaultV, desc string) *StringParameter {
return &StringParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeString,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
}
}
// NewStringParameterWithAuth is a convenience function for initializing a StringParameter with a list of ParamAuthService.
func NewStringParameterWithAuth(name, desc string, authServices []ParamAuthService) *StringParameter {
func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter {
return &StringParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -511,7 +541,7 @@ func (p *StringParameter) GetAuthServices() []ParamAuthService {
}
// NewIntParameter is a convenience function for initializing a IntParameter.
func NewIntParameter(name, desc string) *IntParameter {
func NewIntParameter(name string, desc string) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -522,8 +552,21 @@ func NewIntParameter(name, desc string) *IntParameter {
}
}
// NewIntParameterWithDefault is a convenience function for initializing a IntParameter with default value.
func NewIntParameterWithDefault(name string, defaultV any, desc string) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeInt,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
}
}
// NewIntParameterWithAuth is a convenience function for initializing a IntParameter with a list of ParamAuthService.
func NewIntParameterWithAuth(name, desc string, authServices []ParamAuthService) *IntParameter {
func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -567,7 +610,7 @@ func (p *IntParameter) GetAuthServices() []ParamAuthService {
}
// NewFloatParameter is a convenience function for initializing a FloatParameter.
func NewFloatParameter(name, desc string) *FloatParameter {
func NewFloatParameter(name string, desc string) *FloatParameter {
return &FloatParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -578,8 +621,21 @@ func NewFloatParameter(name, desc string) *FloatParameter {
}
}
// NewFloatParameterWithDefault is a convenience function for initializing a FloatParameter with default value.
func NewFloatParameterWithDefault(name string, defaultV float64, desc string) *FloatParameter {
return &FloatParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeFloat,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
}
}
// NewFloatParameterWithAuth is a convenience function for initializing a FloatParameter with a list of ParamAuthService.
func NewFloatParameterWithAuth(name, desc string, authServices []ParamAuthService) *FloatParameter {
func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter {
return &FloatParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -621,7 +677,7 @@ func (p *FloatParameter) GetAuthServices() []ParamAuthService {
}
// NewBooleanParameter is a convenience function for initializing a BooleanParameter.
func NewBooleanParameter(name, desc string) *BooleanParameter {
func NewBooleanParameter(name string, desc string) *BooleanParameter {
return &BooleanParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -632,8 +688,21 @@ func NewBooleanParameter(name, desc string) *BooleanParameter {
}
}
// NewBooleanParameterWithDefault is a convenience function for initializing a BooleanParameter with default value.
func NewBooleanParameterWithDefault(name string, defaultV bool, desc string) *BooleanParameter {
return &BooleanParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeBool,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
}
}
// NewBooleanParameterWithAuth is a convenience function for initializing a BooleanParameter with a list of ParamAuthService.
func NewBooleanParameterWithAuth(name, desc string, authServices []ParamAuthService) *BooleanParameter {
func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter {
return &BooleanParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -664,7 +733,7 @@ func (p *BooleanParameter) GetAuthServices() []ParamAuthService {
}
// NewArrayParameter is a convenience function for initializing a ArrayParameter.
func NewArrayParameter(name, desc string, items Parameter) *ArrayParameter {
func NewArrayParameter(name string, desc string, items Parameter) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -676,8 +745,22 @@ func NewArrayParameter(name, desc string, items Parameter) *ArrayParameter {
}
}
// NewArrayParameterWithDefault is a convenience function for initializing a ArrayParameter with default value.
func NewArrayParameterWithDefault(name string, defaultV any, desc string, items Parameter) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeArray,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
Items: items,
}
}
// NewArrayParameterWithAuth is a convenience function for initializing a ArrayParameter with a list of ParamAuthService.
func NewArrayParameterWithAuth(name, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
func NewArrayParameterWithAuth(name string, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
@@ -746,9 +829,15 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
authNames[i] = a.Name
}
items := p.Items.Manifest()
var required bool
if p.Default == nil {
required = true
items.Required = true
}
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
Items: &items,

View File

@@ -125,6 +125,100 @@ func TestParametersMarshal(t *testing.T) {
tools.NewArrayParameter("my_array", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
},
},
{
name: "string default",
in: []map[string]any{
{
"name": "my_string",
"type": "string",
"default": "foo",
"description": "this param is a string",
},
},
want: tools.Parameters{
tools.NewStringParameterWithDefault("my_string", "foo", "this param is a string"),
},
},
{
name: "int default",
in: []map[string]any{
{
"name": "my_integer",
"type": "integer",
"default": 5,
"description": "this param is an int",
},
},
want: tools.Parameters{
tools.NewIntParameterWithDefault("my_integer", uint64(5), "this param is an int"),
},
},
{
name: "float default",
in: []map[string]any{
{
"name": "my_float",
"type": "float",
"default": 1.1,
"description": "my param is a float",
},
},
want: tools.Parameters{
tools.NewFloatParameterWithDefault("my_float", 1.1, "my param is a float"),
},
},
{
name: "bool default",
in: []map[string]any{
{
"name": "my_bool",
"type": "boolean",
"default": true,
"description": "this param is a boolean",
},
},
want: tools.Parameters{
tools.NewBooleanParameterWithDefault("my_bool", true, "this param is a boolean"),
},
},
{
name: "string array default",
in: []map[string]any{
{
"name": "my_array",
"type": "array",
"default": `["foo", "bar"]`,
"description": "this param is an array of strings",
"items": map[string]string{
"name": "my_string",
"type": "string",
"description": "string item",
},
},
},
want: tools.Parameters{
tools.NewArrayParameterWithDefault("my_array", `["foo", "bar"]`, "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
},
},
{
name: "float array default",
in: []map[string]any{
{
"name": "my_array",
"type": "array",
"default": "[1.0, 1.1]",
"description": "this param is an array of floats",
"items": map[string]string{
"name": "my_float",
"type": "float",
"description": "float item",
},
},
},
want: tools.Parameters{
tools.NewArrayParameterWithDefault("my_array", "[1.0, 1.1]", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -539,6 +633,46 @@ func TestParametersParse(t *testing.T) {
"my_bool": 1.5,
},
},
{
name: "string default",
params: tools.Parameters{
tools.NewStringParameterWithDefault("my_string", "foo", "this param is a string"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: "foo"}},
},
{
name: "int default",
params: tools.Parameters{
tools.NewIntParameterWithDefault("my_int", 100, "this param is an int"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 100}},
},
{
name: "int (big)",
params: tools.Parameters{
tools.NewIntParameterWithDefault("my_big_int", math.MaxInt64, "this param is an int"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_big_int", Value: math.MaxInt64}},
},
{
name: "float default",
params: tools.Parameters{
tools.NewFloatParameterWithDefault("my_float", 1.1, "this param is a float"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.1}},
},
{
name: "bool default",
params: tools.Parameters{
tools.NewBooleanParameterWithDefault("my_bool", true, "this param is a bool"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -808,22 +942,22 @@ func TestParamManifest(t *testing.T) {
{
name: "string",
in: tools.NewStringParameter("foo-string", "bar"),
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Description: "bar", AuthServices: []string{}},
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}},
},
{
name: "int",
in: tools.NewIntParameter("foo-int", "bar"),
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Description: "bar", AuthServices: []string{}},
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: true, Description: "bar", AuthServices: []string{}},
},
{
name: "float",
in: tools.NewFloatParameter("foo-float", "bar"),
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Description: "bar", AuthServices: []string{}},
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: true, Description: "bar", AuthServices: []string{}},
},
{
name: "boolean",
in: tools.NewBooleanParameter("foo-bool", "bar"),
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Description: "bar", AuthServices: []string{}},
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: true, Description: "bar", AuthServices: []string{}},
},
{
name: "array",
@@ -831,9 +965,42 @@ func TestParamManifest(t *testing.T) {
want: tools.ParameterManifest{
Name: "foo-array",
Type: "array",
Required: true,
Description: "bar",
AuthServices: []string{},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Description: "bar", AuthServices: []string{}},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}},
},
},
{
name: "string default",
in: tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "int default",
in: tools.NewIntParameterWithDefault("foo-int", 1, "bar"),
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "float default",
in: tools.NewFloatParameterWithDefault("foo-float", 1.1, "bar"),
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "boolean default",
in: tools.NewBooleanParameterWithDefault("foo-bool", true, "bar"),
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "array default",
in: tools.NewArrayParameterWithDefault("foo-array", `["foo", "bar"]`, "bar", tools.NewStringParameter("foo-string", "bar")),
want: tools.ParameterManifest{
Name: "foo-array",
Type: "array",
Required: false,
Description: "bar",
AuthServices: []string{},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}},
},
},
}
@@ -893,6 +1060,54 @@ func TestParamMcpManifest(t *testing.T) {
}
}
func TestMcpManifest(t *testing.T) {
tcs := []struct {
name string
in tools.Parameters
want tools.McpToolsSchema
}{
{
name: "string",
in: tools.Parameters{
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
tools.NewStringParameter("foo-string2", "bar"),
tools.NewIntParameterWithDefault("foo-int", 1, "bar"),
tools.NewIntParameter("foo-int2", "bar"),
tools.NewArrayParameterWithDefault("foo-array", []string{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "bar")),
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "bar")),
},
want: tools.McpToolsSchema{
Type: "object",
Properties: map[string]tools.ParameterMcpManifest{
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
"foo-int2": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
"foo-array": tools.ParameterMcpManifest{
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
"foo-array2": tools.ParameterMcpManifest{
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
},
Required: []string{"foo-string2", "foo-int2", "foo-array2"},
},
},
}
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)
}
})
}
}
func TestFailParametersUnmarshal(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {

View File

@@ -118,6 +118,7 @@ func runAiNlToolGetTest(t *testing.T) {
map[string]any{
"name": "question",
"type": "string",
"required": true,
"description": "The natural language question to ask.",
"authSources": []any{},
},