-
Notifications
You must be signed in to change notification settings - Fork 284
mcp: validate tool names #640
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import ( | |
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/google/jsonschema-go/jsonschema" | ||
| ) | ||
|
|
@@ -101,3 +102,26 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawM | |
| } | ||
| return data, nil | ||
| } | ||
|
|
||
| func validateToolName(name string) error { | ||
| if name == "" { | ||
| return fmt.Errorf("tool name cannot be empty") | ||
| } | ||
| if len(name) > 128 { | ||
| return fmt.Errorf("tool name exceeds maximum length of 128 characters (current: %d)", len(name)) | ||
| } | ||
| var invalidChars []string | ||
| seen := make(map[rune]bool) | ||
| for _, r := range name { | ||
| if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == '.') { | ||
|
||
| if !seen[r] { | ||
| invalidChars = append(invalidChars, fmt.Sprintf("%q", string(r))) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use maps.Keys(seen) at the end.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't: need to report characters in the order they occur. Want to be consistent with other SDKs. |
||
| seen[r] = true | ||
| } | ||
| } | ||
| } | ||
| if len(invalidChars) > 0 { | ||
| return fmt.Errorf("tool name contains invalid characters: %s", strings.Join(invalidChars, ", ")) | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,3 +145,53 @@ func TestToolErrorHandling(t *testing.T) { | |
| } | ||
| }) | ||
| } | ||
|
|
||
| func TestValidateToolName(t *testing.T) { | ||
| t.Run("valid", func(t *testing.T) { | ||
| validTests := []struct { | ||
| label string | ||
| toolName string | ||
| }{ | ||
| {"simple alphanumeric names", "getUser"}, | ||
| {"names with underscores", "get_user_profile"}, | ||
| {"names with dashes", "user-profile-update"}, | ||
| {"names with dots", "admin.tools.list"}, | ||
| {"mixed character names", "DATA_EXPORT_v2.1"}, | ||
| {"single character names", "a"}, | ||
| {"128 character names", strings.Repeat("a", 128)}, | ||
| } | ||
| for _, test := range validTests { | ||
| t.Run(test.label, func(t *testing.T) { | ||
| if err := validateToolName(test.toolName); err != nil { | ||
| t.Errorf("validateToolName(%q) = %v, want nil", test.toolName, err) | ||
| } | ||
| }) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("invalid", func(t *testing.T) { | ||
| invalidTests := []struct { | ||
| label string | ||
| toolName string | ||
| wantErrContaining string | ||
| }{ | ||
| {"empty names", "", "tool name cannot be empty"}, | ||
| {"names longer than 128 characters", strings.Repeat("a", 129), "tool name exceeds maximum length of 128 characters (current: 129)"}, | ||
| {"names with spaces", "get user profile", `tool name contains invalid characters: " "`}, | ||
| {"names with commas", "get,user,profile", `tool name contains invalid characters: ","`}, | ||
| {"names with forward slashes", "user/profile/update", `tool name contains invalid characters: "/"`}, | ||
| {"names with other special chars", "[email protected]", `tool name contains invalid characters: "@"`}, | ||
| {"names with multiple invalid chars", "user name@domain,com", `tool name contains invalid characters: " ", "@", ","`}, | ||
| {"names with unicode characters", "user-ñame", `tool name contains invalid characters: "ñ"`}, | ||
| } | ||
| for _, test := range invalidTests { | ||
| t.Run(test.label, func(t *testing.T) { | ||
| if err := validateToolName(test.toolName); err == nil || !strings.Contains(err.Error(), test.wantErrContaining) { | ||
| t.Errorf("validateToolName(%q) = %v, want error containing %q", test.toolName, err, test.wantErrContaining) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| }) | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feel free to ignore: should we include the tool name here to make it easier for debugging? i can also see this being a problem since it is too long. i could see this error message being annoying if the user could have multiple tool names and they don't know which one is too long
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tool name is included at the call site.
An arguable style choice, but I mildly prefer having validators only describe the specific error, because this then leave it up to the caller for how to present that error. For example the caller could do "Adding tool %q: %v"