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") 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. // 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) { func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[string]any) (ParamValues, error) {
params := make([]ParamValue, 0, len(ps)) params := make([]ParamValue, 0, len(ps))
for _, p := range ps { for _, p := range ps {
var v any var v, newV any
var err error
paramAuthServices := p.GetAuthServices() paramAuthServices := p.GetAuthServices()
name := p.GetName() name := p.GetName()
if len(paramAuthServices) == 0 { 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] v, ok = data[name]
if !ok { if !ok {
v = p.GetDefault() 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) return nil, fmt.Errorf("parameter %q is required", name)
} }
} }
} else { } else {
// parse authenticated parameter // parse authenticated parameter
var err error
v, err = parseFromAuthService(paramAuthServices, claimsMap) v, err = parseFromAuthService(paramAuthServices, claimsMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing authenticated parameter %q: %w", name, err) return nil, fmt.Errorf("error parsing authenticated parameter %q: %w", name, err)
} }
} }
newV, err := p.Parse(v) if v != nil {
if err != nil { newV, err = p.Parse(v)
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err) if err != nil {
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
}
} }
params = append(params, ParamValue{Name: name, Value: newV}) params = append(params, ParamValue{Name: name, Value: newV})
} }
@@ -248,6 +256,7 @@ type Parameter interface {
GetName() string GetName() string
GetType() string GetType() string
GetDefault() any GetDefault() any
GetRequired() bool
GetAuthServices() []ParamAuthService GetAuthServices() []ParamAuthService
Parse(any) (any, error) Parse(any) (any, error)
Manifest() ParameterManifest Manifest() ParameterManifest
@@ -378,7 +387,7 @@ func (ps Parameters) McpManifest() McpToolsSchema {
name := p.GetName() name := p.GetName()
properties[name] = p.McpManifest() properties[name] = p.McpManifest()
// parameters that doesn't have a default value are added to the required field // 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) required = append(required, name)
} }
} }
@@ -412,6 +421,7 @@ type CommonParameter struct {
Name string `yaml:"name" validate:"required"` Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"` Type string `yaml:"type" validate:"required"`
Desc string `yaml:"description" validate:"required"` Desc string `yaml:"description" validate:"required"`
Required *bool `yaml:"required"`
AuthServices []ParamAuthService `yaml:"authServices"` AuthServices []ParamAuthService `yaml:"authServices"`
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility. AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
} }
@@ -426,6 +436,15 @@ func (p *CommonParameter) GetType() string {
return p.Type 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. // McpManifest returns the MCP manifest for the Parameter.
func (p *CommonParameter) McpManifest() ParameterMcpManifest { func (p *CommonParameter) McpManifest() ParameterMcpManifest {
return 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. // NewStringParameterWithAuth is a convenience function for initializing a StringParameter with a list of ParamAuthService.
func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter { func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter {
return &StringParameter{ return &StringParameter{
@@ -522,11 +554,11 @@ func (p *StringParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices { for i, a := range p.AuthServices {
authNames[i] = a.Name authNames[i] = a.Name
} }
required := p.Default == nil r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{ return ParameterManifest{
Name: p.Name, Name: p.Name,
Type: p.Type, Type: p.Type,
Required: required, Required: r,
Description: p.Desc, Description: p.Desc,
AuthServices: authNames, 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. // NewIntParameterWithAuth is a convenience function for initializing a IntParameter with a list of ParamAuthService.
func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter { func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter {
return &IntParameter{ return &IntParameter{
@@ -616,11 +661,11 @@ func (p *IntParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices { for i, a := range p.AuthServices {
authNames[i] = a.Name authNames[i] = a.Name
} }
required := p.Default == nil r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{ return ParameterManifest{
Name: p.Name, Name: p.Name,
Type: p.Type, Type: p.Type,
Required: required, Required: r,
Description: p.Desc, Description: p.Desc,
AuthServices: authNames, 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. // NewFloatParameterWithAuth is a convenience function for initializing a FloatParameter with a list of ParamAuthService.
func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter { func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter {
return &FloatParameter{ return &FloatParameter{
@@ -708,11 +766,11 @@ func (p *FloatParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices { for i, a := range p.AuthServices {
authNames[i] = a.Name authNames[i] = a.Name
} }
required := p.Default == nil r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{ return ParameterManifest{
Name: p.Name, Name: p.Name,
Type: p.Type, Type: p.Type,
Required: required, Required: r,
Description: p.Desc, Description: p.Desc,
AuthServices: authNames, 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. // NewBooleanParameterWithAuth is a convenience function for initializing a BooleanParameter with a list of ParamAuthService.
func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter { func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter {
return &BooleanParameter{ return &BooleanParameter{
@@ -789,11 +860,11 @@ func (p *BooleanParameter) Manifest() ParameterManifest {
for i, a := range p.AuthServices { for i, a := range p.AuthServices {
authNames[i] = a.Name authNames[i] = a.Name
} }
required := p.Default == nil r := CheckParamRequired(p.GetRequired(), p.GetDefault())
return ParameterManifest{ return ParameterManifest{
Name: p.Name, Name: p.Name,
Type: p.Type, Type: p.Type,
Required: required, Required: r,
Description: p.Desc, Description: p.Desc,
AuthServices: authNames, 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. // 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 { func NewArrayParameterWithAuth(name string, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
return &ArrayParameter{ return &ArrayParameter{
@@ -910,12 +995,13 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
authNames[i] = a.Name authNames[i] = a.Name
} }
items := p.Items.Manifest() items := p.Items.Manifest()
required := p.Default == nil // if required value is true, or there's no default value
items.Required = required r := CheckParamRequired(p.GetRequired(), p.GetDefault())
items.Required = r
return ParameterManifest{ return ParameterManifest{
Name: p.Name, Name: p.Name,
Type: p.Type, Type: p.Type,
Required: required, Required: r,
Description: p.Desc, Description: p.Desc,
AuthServices: authNames, AuthServices: authNames,
Items: &items, Items: &items,

View File

@@ -50,6 +50,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewStringParameter("my_string", "this param is a string"), 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", name: "int",
in: []map[string]any{ in: []map[string]any{
@@ -63,6 +77,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewIntParameter("my_integer", "this param is an int"), 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", name: "float",
in: []map[string]any{ in: []map[string]any{
@@ -76,6 +104,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewFloatParameter("my_float", "my param is a float"), 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", name: "bool",
in: []map[string]any{ in: []map[string]any{
@@ -89,6 +131,20 @@ func TestParametersMarshal(t *testing.T) {
tools.NewBooleanParameter("my_bool", "this param is a boolean"), 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", name: "string array",
in: []map[string]any{ 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")), 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", name: "float array",
in: []map[string]any{ in: []map[string]any{
@@ -673,6 +748,38 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{}, in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}}, 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 { for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) { 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{}}, 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 { for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -1071,6 +1210,8 @@ func TestMcpManifest(t *testing.T) {
in: tools.Parameters{ in: tools.Parameters{
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"), tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
tools.NewStringParameter("foo-string2", "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.NewIntParameterWithDefault("foo-int", 1, "bar"),
tools.NewIntParameter("foo-int2", "bar"), tools.NewIntParameter("foo-int2", "bar"),
tools.NewArrayParameterWithDefault("foo-array", []any{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "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{ want: tools.McpToolsSchema{
Type: "object", Type: "object",
Properties: map[string]tools.ParameterMcpManifest{ Properties: map[string]tools.ParameterMcpManifest{
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"}, "foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"}, "foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"}, "foo-string-req": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-int2": tools.ParameterMcpManifest{Type: "integer", 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-array": tools.ParameterMcpManifest{
Type: "array", Type: "array",
Description: "bar", Description: "bar",
@@ -1094,7 +1237,7 @@ func TestMcpManifest(t *testing.T) {
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"}, 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)
}
})
}
}