feat: add support for null optional parameter (#802)

Added an option for user to indicate if the parameter is required. 

Example:
```
parameters:
  - name: foo
    type: string
    description: foo
    required: false
```

If the `required` field is not provided, it will be defaulted to `true`.


If a `default` value is provided, `required = false` regardless if the
field is indicated.
```
parameters:
  - name: foo
    type: string
    description: foo
    default: hello world
```

Fixes #736
This commit is contained in:
Yuan Teoh
2025-07-14 23:41:49 -05:00
committed by GitHub
parent 8ce311f256
commit a817b120ca
2 changed files with 294 additions and 23 deletions

View File

@@ -109,11 +109,17 @@ func parseFromAuthService(paramAuthServices []ParamAuthService, claimsMap map[st
return nil, fmt.Errorf("missing or invalid authentication header")
}
// CheckParamRequired checks if a parameter is required based on the required and default field.
func CheckParamRequired(required bool, defaultV any) bool {
return required && defaultV == nil
}
// ParseParams is a helper function for parsing Parameters from an arbitraryJSON object.
func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[string]any) (ParamValues, error) {
params := make([]ParamValue, 0, len(ps))
for _, p := range ps {
var v any
var v, newV any
var err error
paramAuthServices := p.GetAuthServices()
name := p.GetName()
if len(paramAuthServices) == 0 {
@@ -122,21 +128,23 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
v, ok = data[name]
if !ok {
v = p.GetDefault()
if v == nil {
// if the parameter is required and no value given, throw an error
if CheckParamRequired(p.GetRequired(), v) {
return nil, fmt.Errorf("parameter %q is required", name)
}
}
} else {
// parse authenticated parameter
var err error
v, err = parseFromAuthService(paramAuthServices, claimsMap)
if err != nil {
return nil, fmt.Errorf("error parsing authenticated parameter %q: %w", name, err)
}
}
newV, err := p.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
if v != nil {
newV, err = p.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
}
}
params = append(params, ParamValue{Name: name, Value: newV})
}
@@ -248,6 +256,7 @@ type Parameter interface {
GetName() string
GetType() string
GetDefault() any
GetRequired() bool
GetAuthServices() []ParamAuthService
Parse(any) (any, error)
Manifest() ParameterManifest
@@ -378,7 +387,7 @@ func (ps Parameters) McpManifest() McpToolsSchema {
name := p.GetName()
properties[name] = p.McpManifest()
// parameters that doesn't have a default value are added to the required field
if p.GetDefault() == nil {
if CheckParamRequired(p.GetRequired(), p.GetDefault()) {
required = append(required, name)
}
}
@@ -412,6 +421,7 @@ type CommonParameter struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Desc string `yaml:"description" validate:"required"`
Required *bool `yaml:"required"`
AuthServices []ParamAuthService `yaml:"authServices"`
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
}
@@ -426,6 +436,15 @@ func (p *CommonParameter) GetType() string {
return p.Type
}
// GetRequired returns the type specified for the Parameter.
func (p *CommonParameter) GetRequired() bool {
// parameters are defaulted to required
if p.Required == nil {
return true
}
return *p.Required
}
// McpManifest returns the MCP manifest for the Parameter.
func (p *CommonParameter) McpManifest() ParameterMcpManifest {
return ParameterMcpManifest{
@@ -475,6 +494,19 @@ func NewStringParameterWithDefault(name string, defaultV, desc string) *StringPa
}
}
// NewStringParameterWithRequired is a convenience function for initializing a StringParameter.
func NewStringParameterWithRequired(name string, desc string, required bool) *StringParameter {
return &StringParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeString,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewStringParameterWithAuth is a convenience function for initializing a StringParameter with a list of ParamAuthService.
func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter {
return &StringParameter{
@@ -522,11 +554,11 @@ func (p *StringParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Required: r,
Description: p.Desc,
AuthServices: authNames,
}
@@ -557,6 +589,19 @@ func NewIntParameterWithDefault(name string, defaultV int, desc string) *IntPara
}
}
// NewIntParameterWithRequired is a convenience function for initializing a IntParameter.
func NewIntParameterWithRequired(name string, desc string, required bool) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeInt,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewIntParameterWithAuth is a convenience function for initializing a IntParameter with a list of ParamAuthService.
func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter {
return &IntParameter{
@@ -616,11 +661,11 @@ func (p *IntParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Required: r,
Description: p.Desc,
AuthServices: authNames,
}
@@ -651,6 +696,19 @@ func NewFloatParameterWithDefault(name string, defaultV float64, desc string) *F
}
}
// NewFloatParameterWithRequired is a convenience function for initializing a FloatParameter.
func NewFloatParameterWithRequired(name string, desc string, required bool) *FloatParameter {
return &FloatParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeFloat,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewFloatParameterWithAuth is a convenience function for initializing a FloatParameter with a list of ParamAuthService.
func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter {
return &FloatParameter{
@@ -708,11 +766,11 @@ func (p *FloatParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Required: r,
Description: p.Desc,
AuthServices: authNames,
}
@@ -743,6 +801,19 @@ func NewBooleanParameterWithDefault(name string, defaultV bool, desc string) *Bo
}
}
// NewBooleanParameterWithRequired is a convenience function for initializing a BooleanParameter.
func NewBooleanParameterWithRequired(name string, desc string, required bool) *BooleanParameter {
return &BooleanParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeBool,
Desc: desc,
Required: &required,
AuthServices: nil,
},
}
}
// NewBooleanParameterWithAuth is a convenience function for initializing a BooleanParameter with a list of ParamAuthService.
func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter {
return &BooleanParameter{
@@ -789,11 +860,11 @@ func (p *BooleanParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Required: r,
Description: p.Desc,
AuthServices: authNames,
}
@@ -826,6 +897,20 @@ func NewArrayParameterWithDefault(name string, defaultV []any, desc string, item
}
}
// NewArrayParameterWithRequired is a convenience function for initializing a ArrayParameter with default value.
func NewArrayParameterWithRequired(name string, desc string, required bool, items Parameter) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeArray,
Desc: desc,
Required: &required,
AuthServices: nil,
},
Items: items,
}
}
// NewArrayParameterWithAuth is a convenience function for initializing a ArrayParameter with a list of ParamAuthService.
func NewArrayParameterWithAuth(name string, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
return &ArrayParameter{
@@ -910,12 +995,13 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
authNames[i] = a.Name
}
items := p.Items.Manifest()
required := p.Default == nil
items.Required = required
// if required value is true, or there's no default value
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
items.Required = r
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Required: r,
Description: p.Desc,
AuthServices: authNames,
Items: &items,

View File

@@ -50,6 +50,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewStringParameter("my_string", "this param is a string"),
},
},
{
name: "string not required",
in: []map[string]any{
{
"name": "my_string",
"type": "string",
"description": "this param is a string",
"required": false,
},
},
want: tools.Parameters{
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
},
},
{
name: "int",
in: []map[string]any{
@@ -63,6 +77,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewIntParameter("my_integer", "this param is an int"),
},
},
{
name: "int not required",
in: []map[string]any{
{
"name": "my_integer",
"type": "integer",
"description": "this param is an int",
"required": false,
},
},
want: tools.Parameters{
tools.NewIntParameterWithRequired("my_integer", "this param is an int", false),
},
},
{
name: "float",
in: []map[string]any{
@@ -76,6 +104,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewFloatParameter("my_float", "my param is a float"),
},
},
{
name: "float not required",
in: []map[string]any{
{
"name": "my_float",
"type": "float",
"description": "my param is a float",
"required": false,
},
},
want: tools.Parameters{
tools.NewFloatParameterWithRequired("my_float", "my param is a float", false),
},
},
{
name: "bool",
in: []map[string]any{
@@ -89,6 +131,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewBooleanParameter("my_bool", "this param is a boolean"),
},
},
{
name: "bool not required",
in: []map[string]any{
{
"name": "my_bool",
"type": "boolean",
"description": "this param is a boolean",
"required": false,
},
},
want: tools.Parameters{
tools.NewBooleanParameterWithRequired("my_bool", "this param is a boolean", false),
},
},
{
name: "string array",
in: []map[string]any{
@@ -107,6 +163,25 @@ func TestParametersMarshal(t *testing.T) {
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
},
},
{
name: "string array not required",
in: []map[string]any{
{
"name": "my_array",
"type": "array",
"description": "this param is an array of strings",
"required": false,
"items": map[string]string{
"name": "my_string",
"type": "string",
"description": "string item",
},
},
},
want: tools.Parameters{
tools.NewArrayParameterWithRequired("my_array", "this param is an array of strings", false, tools.NewStringParameter("my_string", "string item")),
},
},
{
name: "float array",
in: []map[string]any{
@@ -673,6 +748,38 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
},
{
name: "string not required",
params: tools.Parameters{
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: nil}},
},
{
name: "int not required",
params: tools.Parameters{
tools.NewIntParameterWithRequired("my_int", "this param is an int", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: nil}},
},
{
name: "float not required",
params: tools.Parameters{
tools.NewFloatParameterWithRequired("my_float", "this param is a float", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: nil}},
},
{
name: "bool not required",
params: tools.Parameters{
tools.NewBooleanParameterWithRequired("my_bool", "this param is a bool", false),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: nil}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -1003,6 +1110,38 @@ func TestParamManifest(t *testing.T) {
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
},
{
name: "string not required",
in: tools.NewStringParameterWithRequired("foo-string", "bar", false),
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "int not required",
in: tools.NewIntParameterWithRequired("foo-int", "bar", false),
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "float not required",
in: tools.NewFloatParameterWithRequired("foo-float", "bar", false),
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "boolean not required",
in: tools.NewBooleanParameterWithRequired("foo-bool", "bar", false),
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}},
},
{
name: "array not required",
in: tools.NewArrayParameterWithRequired("foo-array", "bar", false, 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: false, Description: "bar", AuthServices: []string{}},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -1071,6 +1210,8 @@ func TestMcpManifest(t *testing.T) {
in: tools.Parameters{
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
tools.NewStringParameter("foo-string2", "bar"),
tools.NewStringParameterWithRequired("foo-string-req", "bar", true),
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")),
@@ -1079,10 +1220,12 @@ func TestMcpManifest(t *testing.T) {
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-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{
Type: "array",
Description: "bar",
@@ -1094,7 +1237,7 @@ func TestMcpManifest(t *testing.T) {
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
},
Required: []string{"foo-string2", "foo-int2", "foo-array2"},
Required: []string{"foo-string2", "foo-string-req", "foo-int2", "foo-array2"},
},
},
}
@@ -1502,3 +1645,45 @@ func TestFailResolveTemplateParameters(t *testing.T) {
})
}
}
func TestCheckParamRequired(t *testing.T) {
tcs := []struct {
name string
required bool
defaultV any
want bool
}{
{
name: "required and no default",
required: true,
defaultV: nil,
want: true,
},
{
name: "required and default",
required: true,
defaultV: "foo",
want: false,
},
{
name: "not required and no default",
required: false,
defaultV: nil,
want: false,
},
{
name: "not required and default",
required: false,
defaultV: "foo",
want: false,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tools.CheckParamRequired(tc.required, tc.defaultV)
if got != tc.want {
t.Fatalf("got %v, want %v", got, tc.want)
}
})
}
}