feat: implement DNS namespace validation and remote URL uniqueness checks (#306)

## Summary

- Add reverse-DNS namespace format validation for server names
(namespace/name)
- Validate remote URLs match the DNS namespace derived from hostname  
- Prevent duplicate remote URLs across different servers
- Support localhost/development URLs without namespace restrictions
- Add comprehensive test coverage for validation logic
- Update database layer to support remote_url filtering

Fixes #180 by ensuring proper namespace governance and preventing URL
conflicts.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
adam jones 2025-08-26 23:03:38 +01:00 • committed by GitHub
commit 923b2e07b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 497 additions and 29 deletions

View file

@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"fmt"
"time"
"github.com/google/uuid"
@ -78,11 +79,21 @@ func (s *registryServiceImpl) Publish(req model.PublishRequest) (*model.ServerRe
return nil, err
}
// Validate server name exists
// Validate server name exists and format
if _, err := model.ParseServerName(req.Server); err != nil {
return nil, err
}
// Validate reverse-DNS namespace matching for remote URLs
if err := model.ValidateRemoteNamespaceMatch(req.Server); err != nil {
return nil, err
}
// Check for duplicate remote URLs
if err := s.validateNoDuplicateRemoteURLs(ctx, req.Server); err != nil {
return nil, err
}
// Get the new version's details
newVersion := req.Server.VersionDetail.Version
newName := req.Server.Name
@ -154,3 +165,28 @@ func (s *registryServiceImpl) Publish(req model.PublishRequest) (*model.ServerRe
response := serverRecord.ToServerResponse()
return &response, nil
}
// validateNoDuplicateRemoteURLs checks that no other server is using the same remote URLs
func (s *registryServiceImpl) validateNoDuplicateRemoteURLs(ctx context.Context, serverDetail model.ServerDetail) error {
// Check each remote URL in the new server for conflicts
for _, remote := range serverDetail.Remotes {
// Use filter to find servers with this remote URL
filter := map[string]any{
"remote_url": remote.URL,
}
conflictingServers, _, err := s.db.List(ctx, filter, "", 1000)
if err != nil {
return fmt.Errorf("failed to check remote URL conflict: %w", err)
}
// Check if any conflicting server has a different name
for _, conflictingServer := range conflictingServers {
if conflictingServer.ServerJSON.Name != serverDetail.Name {
return fmt.Errorf("remote URL %s is already used by server %s", remote.URL, conflictingServer.ServerJSON.Name)
}
}
}
return nil
}

View file

@ -0,0 +1,121 @@
//nolint:testpackage
package service
import (
"context"
"testing"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/stretchr/testify/assert"
)
func TestValidateNoDuplicateRemoteURLs(t *testing.T) {
// Create test data
existingServers := map[string]*model.ServerDetail{
"existing1": {
Name: "com.example/existing-server",
Description: "An existing server",
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Remotes: []model.Remote{
{URL: "https://api.example.com/mcp"},
{URL: "https://webhook.example.com/sse"},
},
},
"existing2": {
Name: "com.microsoft/another-server",
Description: "Another existing server",
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Remotes: []model.Remote{
{URL: "https://api.microsoft.com/mcp"},
},
},
}
memDB := database.NewMemoryDB(existingServers)
service := NewRegistryServiceWithDB(memDB)
tests := []struct {
name string
serverDetail model.ServerDetail
expectError bool
errorMsg string
}{
{
name: "no remote URLs - should pass",
serverDetail: model.ServerDetail{
Name: "com.example/new-server",
Description: "A new server with no remotes",
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Remotes: []model.Remote{},
},
expectError: false,
},
{
name: "new unique remote URLs - should pass",
serverDetail: model.ServerDetail{
Name: "com.example/new-server",
Description: "A new server",
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Remotes: []model.Remote{
{URL: "https://new.example.com/mcp"},
{URL: "https://unique.example.com/sse"},
},
},
expectError: false,
},
{
name: "duplicate remote URL - should fail",
serverDetail: model.ServerDetail{
Name: "com.example/new-server",
Description: "A new server with duplicate URL",
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Remotes: []model.Remote{
{URL: "https://api.example.com/mcp"}, // This URL already exists
},
},
expectError: true,
errorMsg: "remote URL https://api.example.com/mcp is already used by server com.example/existing-server",
},
{
name: "updating same server with same URLs - should pass",
serverDetail: model.ServerDetail{
Name: "com.example/existing-server", // Same name as existing
Description: "Updated existing server",
VersionDetail: model.VersionDetail{
Version: "1.1.0",
},
Remotes: []model.Remote{
{URL: "https://api.example.com/mcp"}, // Same URL as before
},
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
impl := service.(*registryServiceImpl)
err := impl.validateNoDuplicateRemoteURLs(ctx, tt.serverDetail)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}