From 24d791d12726cadfab4d89e854454318b3591ccc Mon Sep 17 00:00:00 2001 From: anneheartrecord Date: Fri, 12 Jun 2026 23:45:12 +0800 Subject: [PATCH] fix(validators): allow GitLab repository URLs with nested subgroups --- internal/validators/utils.go | 3 +- internal/validators/utils_test.go | 57 ++++++++++++++++++++++++++ internal/validators/validators_test.go | 14 +++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 internal/validators/utils_test.go diff --git a/internal/validators/utils.go b/internal/validators/utils.go index 035132db..0e3feaf9 100644 --- a/internal/validators/utils.go +++ b/internal/validators/utils.go @@ -10,8 +10,9 @@ var ( // Regular expressions for validating repository URLs // These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository // For example: // - GitHub: https://github.com/user/repo + // GitLab additionally supports nested groups/subgroups, e.g. https://gitlab.com/group/subgroup/repo githubURLRegex = regexp.MustCompile(`^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$`) - gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+/[\w.-]+/?$`) + gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+(/[\w.-]+)+/?$`) ) // IsValidRepositoryURL checks if the given URL is valid for the specified repository source diff --git a/internal/validators/utils_test.go b/internal/validators/utils_test.go new file mode 100644 index 00000000..b70197b0 --- /dev/null +++ b/internal/validators/utils_test.go @@ -0,0 +1,57 @@ +package validators_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/modelcontextprotocol/registry/internal/validators" +) + +func TestIsValidRepositoryURL(t *testing.T) { + tests := []struct { + name string + source validators.RepositorySource + url string + want bool + }{ + // GitHub + {"github valid owner/repo", validators.SourceGitHub, "https://github.com/owner/repo", true}, + {"github valid with trailing slash", validators.SourceGitHub, "https://github.com/owner/repo/", true}, + {"github valid with www", validators.SourceGitHub, "https://www.github.com/owner/repo", true}, + {"github missing repo", validators.SourceGitHub, "https://github.com/owner", false}, + {"github extra path segment stays invalid", validators.SourceGitHub, "https://github.com/owner/repo/extra", false}, + + // GitLab: flat owner/repo (no regression) + {"gitlab valid owner/repo", validators.SourceGitLab, "https://gitlab.com/owner/repo", true}, + {"gitlab valid with trailing slash", validators.SourceGitLab, "https://gitlab.com/owner/repo/", true}, + {"gitlab valid with www", validators.SourceGitLab, "https://www.gitlab.com/owner/repo", true}, + {"gitlab valid with dots and dashes", validators.SourceGitLab, "https://gitlab.com/my-org.io/my_repo-v2.0", true}, + + // GitLab: nested groups/subgroups (issue #1359) + {"gitlab valid single subgroup", validators.SourceGitLab, "https://gitlab.com/group/subgroup/repo", true}, + {"gitlab valid deeply nested subgroups", validators.SourceGitLab, "https://gitlab.com/myorg/team/subgroup/my-mcp-server", true}, + {"gitlab valid nested with trailing slash", validators.SourceGitLab, "https://gitlab.com/group/subgroup/repo/", true}, + + // GitLab: malformed URLs that must stay rejected + {"gitlab missing owner and repo", validators.SourceGitLab, "https://gitlab.com", false}, + {"gitlab missing repo", validators.SourceGitLab, "https://gitlab.com/owner", false}, + {"gitlab empty path segment", validators.SourceGitLab, "https://gitlab.com/group//repo", false}, + {"gitlab leading empty segment", validators.SourceGitLab, "https://gitlab.com//group/repo", false}, + {"gitlab segment with space", validators.SourceGitLab, "https://gitlab.com/group/sub group/repo", false}, + {"gitlab spoofed host suffix", validators.SourceGitLab, "https://evilgitlab.com/group/repo", false}, + {"gitlab spoofed host prefix", validators.SourceGitLab, "https://gitlab.com.evil.com/group/repo", false}, + {"gitlab query string", validators.SourceGitLab, "https://gitlab.com/group/subgroup/repo?ref=main", false}, + {"gitlab fragment", validators.SourceGitLab, "https://gitlab.com/group/subgroup/repo#readme", false}, + + // Unknown source + {"unknown source", validators.RepositorySource("bitbucket"), "https://bitbucket.org/owner/repo", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := validators.IsValidRepositoryURL(tc.source, tc.url) + assert.Equal(t, tc.want, got, "url: %s", tc.url) + }) + } +} diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index c3e5585b..12ca281c 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -323,6 +323,20 @@ func TestValidate(t *testing.T) { }, expectedError: validators.ErrInvalidRepositoryURL.Error(), }, + { + name: "server with valid GitLab subgroup URL", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://gitlab.com/group/subgroup/repo", + Source: "gitlab", + }, + Version: "1.0.0", + }, + expectedError: "", + }, { name: "server with valid repository subfolder", serverDetail: apiv0.ServerJSON{