Thanks Camila for being a great Go consultant and advising on how to
tidy this up!
(I'm still a lil grumpy about some parts, but it's in much better shape
😄)
- Consolidated publisher extension validation into a single function.
- Updated the `Publish` and `EditServer` methods to directly use
`XPublisher` from the request.
- Removed outdated tests for publisher extension extraction and
validation.
- Introduced new validation functions for server details and remote URL
matching.
- Enhanced error handling for server name format and remote URL
validation.
- Updated existing tests to reflect changes in validation logic and
structure.
## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Things were starting to become a bit spaghetti-y after lots of small PRs
and contributions from many people. Camila and I went through and
figured out how we could make this a lot cleaner.
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
Tested locally, plus all the integration and unit tests still pass.
## Breaking Changes
<!-- Will users need to update their code or configurations? -->
None
---------
Co-authored-by: Claude <noreply@anthropic.com>
176 lines
5.2 KiB
Go
176 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/modelcontextprotocol/registry/internal/database"
|
|
"github.com/modelcontextprotocol/registry/internal/validators"
|
|
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
|
"github.com/modelcontextprotocol/registry/pkg/model"
|
|
)
|
|
|
|
// fakeRegistryService implements RegistryService interface with an in-memory database
|
|
type fakeRegistryService struct {
|
|
db *database.MemoryDB
|
|
}
|
|
|
|
// NewFakeRegistryService creates a new fake registry service with pre-populated data
|
|
func NewFakeRegistryService() RegistryService {
|
|
// Sample registry entries with updated model structure using ServerJSON
|
|
serverDetails := []*model.ServerJSON{
|
|
{
|
|
Name: "bluegreen/mcp-server",
|
|
Description: "A dummy MCP registry for testing",
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/mcp-1",
|
|
Source: "github",
|
|
ID: "example/mcp-1",
|
|
},
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
{
|
|
Name: "orangepurple/mcp-server",
|
|
Description: "Another dummy MCP registry for testing",
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/mcp-2",
|
|
Source: "github",
|
|
ID: "example/mcp-2",
|
|
},
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "0.9.0",
|
|
},
|
|
},
|
|
{
|
|
Name: "greenyellow/mcp-server",
|
|
Description: "Yet another dummy MCP registry for testing",
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/mcp-3",
|
|
Source: "github",
|
|
ID: "example/mcp-3",
|
|
},
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "0.9.5",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create a new in-memory database using registry metadata IDs
|
|
serverDetailMap := make(map[string]*model.ServerJSON)
|
|
for _, entry := range serverDetails {
|
|
registryID := uuid.New().String() // Generate registry metadata ID
|
|
serverDetailMap[registryID] = entry
|
|
}
|
|
memDB := database.NewMemoryDB(serverDetailMap)
|
|
return &fakeRegistryService{
|
|
db: memDB,
|
|
}
|
|
}
|
|
|
|
// List retrieves servers with extension wrapper format
|
|
func (s *fakeRegistryService) List(cursor string, limit int) ([]apiv0.ServerRecord, string, error) {
|
|
// Create a timeout context for the database operation
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Use the database's List method to get ServerRecord entries
|
|
serverRecords, nextCursor, err := s.db.List(ctx, nil, cursor, limit)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Return ServerRecords directly (they're now the same as ServerResponse)
|
|
result := make([]apiv0.ServerRecord, len(serverRecords))
|
|
for i, record := range serverRecords {
|
|
result[i] = *record
|
|
}
|
|
|
|
return result, nextCursor, nil
|
|
}
|
|
|
|
// GetByID retrieves a specific server by its registry metadata ID in extension wrapper format
|
|
func (s *fakeRegistryService) GetByID(id string) (*apiv0.ServerRecord, error) {
|
|
// Create a timeout context for the database operation
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Use the database's GetByID method to retrieve the server record
|
|
serverRecord, err := s.db.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return ServerRecord directly (it's now the same as ServerResponse)
|
|
return serverRecord, nil
|
|
}
|
|
|
|
// Publish publishes a server with separated extensions
|
|
func (s *fakeRegistryService) Publish(req apiv0.PublishRequest) (*apiv0.ServerRecord, error) {
|
|
// Create a timeout context for the database operation
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Validate the request
|
|
if err := validators.ValidatePublishRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use publisher extensions directly from request
|
|
publisherExtensions := req.XPublisher
|
|
if publisherExtensions == nil {
|
|
publisherExtensions = make(map[string]interface{})
|
|
}
|
|
|
|
// Create registry metadata for fake service (always marks as latest)
|
|
now := time.Now()
|
|
registryMetadata := apiv0.RegistryExtensions{
|
|
ID: uuid.New().String(),
|
|
PublishedAt: now,
|
|
UpdatedAt: now,
|
|
IsLatest: true,
|
|
ReleaseDate: now.Format(time.RFC3339),
|
|
}
|
|
|
|
// Publish to database
|
|
serverRecord, err := s.db.Publish(ctx, req.Server, publisherExtensions, registryMetadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return ServerRecord directly (it's now the same as ServerResponse)
|
|
return serverRecord, nil
|
|
}
|
|
|
|
// EditServer updates an existing server with new details (admin operation)
|
|
func (s *fakeRegistryService) EditServer(id string, req apiv0.PublishRequest) (*apiv0.ServerRecord, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Validate the request
|
|
if err := validators.ValidatePublishRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use publisher extensions directly from request
|
|
publisherExtensions := req.XPublisher
|
|
if publisherExtensions == nil {
|
|
publisherExtensions = make(map[string]interface{})
|
|
}
|
|
|
|
// Update server in database
|
|
serverRecord, err := s.db.UpdateServer(ctx, id, req.Server, publisherExtensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return ServerRecord directly (it's now the same as ServerResponse)
|
|
return serverRecord, nil
|
|
}
|
|
|
|
// Close closes the in-memory database connection
|
|
func (s *fakeRegistryService) Close() error {
|
|
return s.db.Close()
|
|
}
|