Skip to content

Commit 419362f

Browse files
committed
feat: add server name format validation at API layer
- Enforce validation matching database constraint from migration 008 - Namespace must match: [a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9] - Name must match: [a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9] - Provide clear error messages indicating which part is invalid - Use DRY approach with composed regex patterns
1 parent fead5b0 commit 419362f

File tree

3 files changed

+33
-3
lines changed

3 files changed

+33
-3
lines changed

internal/database/migrations/008_clean_invalid_data.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ BEGIN
5555
RAISE NOTICE ' Total servers in database: %', total_servers;
5656

5757
IF total_servers > 0 THEN
58-
RAISE NOTICE ' Servers to DELETE: % (%.2f%%)', total_to_delete, (total_to_delete::float / total_servers * 100);
58+
RAISE NOTICE ' Servers to DELETE: % (% percent of total)', total_to_delete, ROUND((total_to_delete::numeric / total_servers * 100), 2);
5959
ELSE
6060
RAISE NOTICE ' Servers to DELETE: %', total_to_delete;
6161
END IF;
@@ -85,7 +85,7 @@ BEGIN
8585
OR value->>'version' = ''
8686
ORDER BY value->>'name'
8787
LOOP
88-
RAISE NOTICE ' - %@% (reason: %)', r.name, r.version, r.reason;
88+
RAISE NOTICE ' - % @ % (reason: %)', r.name, COALESCE(r.version, '<NULL>'), r.reason;
8989
END LOOP;
9090

9191
RAISE EXCEPTION 'Safety check failed: Expected to delete exactly 5 servers but would delete %. Check the log above for details. Aborting to prevent data loss.', total_to_delete;
@@ -102,7 +102,7 @@ BEGIN
102102
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted')
103103
ORDER BY value->>'name'
104104
LOOP
105-
RAISE NOTICE ' - %@% (current status: %)', r.name, r.version, r.status;
105+
RAISE NOTICE ' - % @ % (current status: %)', r.name, r.version, r.status;
106106
END LOOP;
107107

108108
RAISE EXCEPTION 'Safety check failed: Expected to update exactly 1 server status but would update %. Check the log above for details. Aborting to prevent data corruption.', invalid_status_count;

internal/validators/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var (
2828

2929
// Server name validation errors
3030
ErrMultipleSlashesInServerName = errors.New("server name cannot contain multiple slashes")
31+
ErrInvalidServerNameFormat = errors.New("server name format is invalid")
3132
)
3233

3334
// RepositorySource represents valid repository sources

internal/validators/validators.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ import (
1414
"github.com/modelcontextprotocol/registry/pkg/model"
1515
)
1616

17+
// Server name validation patterns
18+
var (
19+
// Component patterns for namespace and name parts
20+
namespacePattern = `[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]`
21+
namePartPattern = `[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]`
22+
23+
// Compiled regexes
24+
namespaceRegex = regexp.MustCompile(`^` + namespacePattern + `$`)
25+
namePartRegex = regexp.MustCompile(`^` + namePartPattern + `$`)
26+
serverNameRegex = regexp.MustCompile(`^` + namespacePattern + `/` + namePartPattern + `$`)
27+
)
28+
1729
// Regexes to detect semver range syntaxes
1830
var (
1931
// Case 1: comparator ranges
@@ -403,11 +415,28 @@ func parseServerName(serverJSON apiv0.ServerJSON) (string, error) {
403415
return "", ErrMultipleSlashesInServerName
404416
}
405417

418+
// Split and check for empty parts
406419
parts := strings.SplitN(name, "/", 2)
407420
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
408421
return "", fmt.Errorf("server name must be in format 'dns-namespace/name' with non-empty namespace and name parts")
409422
}
410423

424+
// Validate name format using regex
425+
if !serverNameRegex.MatchString(name) {
426+
namespace := parts[0]
427+
serverName := parts[1]
428+
429+
// Check which part is invalid for a better error message
430+
if !namespaceRegex.MatchString(namespace) {
431+
return "", fmt.Errorf("%w: namespace '%s' is invalid. Namespace must start and end with alphanumeric characters, and may contain dots and hyphens in the middle", ErrInvalidServerNameFormat, namespace)
432+
}
433+
if !namePartRegex.MatchString(serverName) {
434+
return "", fmt.Errorf("%w: name '%s' is invalid. Name must start and end with alphanumeric characters, and may contain dots, underscores, and hyphens in the middle", ErrInvalidServerNameFormat, serverName)
435+
}
436+
// Fallback in case both somehow pass individually but not together
437+
return "", fmt.Errorf("%w: invalid format for '%s'", ErrInvalidServerNameFormat, name)
438+
}
439+
411440
return name, nil
412441
}
413442

0 commit comments

Comments
 (0)