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:
parent
d663406280
commit
923b2e07b9
2 changed files with 497 additions and 29 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
121
internal/service/registry_service_test.go
Normal file
121
internal/service/registry_service_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue