diff --git a/internal/server/config.go b/internal/server/config.go index 4df8cea7c85..652e7547de7 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -16,6 +16,7 @@ package server import ( "context" "fmt" + "regexp" "strings" yaml "github.com/goccy/go-yaml" @@ -272,6 +273,10 @@ func (c *ToolConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interfac } for name, u := range raw { + err := NameValidation(name) + if err != nil { + return err + } var v map[string]any if err := u.Unmarshal(&v); err != nil { return fmt.Errorf("unable to unmarshal %q: %w", name, err) @@ -432,3 +437,23 @@ func (c *PromptsetConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(int } return nil } + +// Tools naming validation is added in the MCP v2025-11-25, but we'll be +// implementing it across Toolbox +// Tool names SHOULD be between 1 and 128 characters in length (inclusive). +// Tool names SHOULD be considered case-sensitive. +// The following SHOULD be the only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.) +// Tool names SHOULD NOT contain spaces, commas, or other special characters. +// Tool names SHOULD be unique within a server. +func NameValidation(name string) error { + strLen := len(name) + if strLen < 1 || strLen > 128 { + return fmt.Errorf("resource name SHOULD be between 1 and 128 characters in length (inclusive)") + } + validChars := regexp.MustCompile("^[a-zA-Z0-9_.-]+$") + isValid := validChars.MatchString(name) + if !isValid { + return fmt.Errorf("invalid character for resource name; only uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.) is allowed") + } + return nil +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index b87401202e7..c13df83be41 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -200,3 +200,62 @@ func TestUpdateServer(t *testing.T) { t.Errorf("error updating server, promptset (-want +got):\n%s", diff) } } + +func TestNameValidation(t *testing.T) { + testCases := []struct { + desc string + resourceName string + errStr string + }{ + { + desc: "names with 0 length", + resourceName: "", + errStr: "resource name SHOULD be between 1 and 128 characters in length (inclusive)", + }, + { + desc: "names with allowed length", + resourceName: "foo", + }, + { + desc: "names with 128 length", + resourceName: strings.Repeat("a", 128), + }, + { + desc: "names with more than 128 length", + resourceName: strings.Repeat("a", 129), + errStr: "resource name SHOULD be between 1 and 128 characters in length (inclusive)", + }, + { + desc: "names with space", + resourceName: "foo bar", + errStr: "invalid character for resource name; only uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.) is allowed", + }, + { + desc: "names with commas", + resourceName: "foo,bar", + errStr: "invalid character for resource name; only uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.) is allowed", + }, + { + desc: "names with other special character", + resourceName: "foo!", + errStr: "invalid character for resource name; only uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.) is allowed", + }, + { + desc: "names with allowed special character", + resourceName: "foo_.-bar6", + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + err := server.NameValidation(tc.resourceName) + if err != nil { + if tc.errStr != err.Error() { + t.Fatalf("unexpected error: %s", err) + } + } + if err == nil && tc.errStr != "" { + t.Fatalf("expect error: %s", tc.errStr) + } + }) + } +}