## Summary
Implements `PUT /v0/servers/{id}` admin edit endpoint to address part of
issue #182, providing an API-based solution for server moderation
without requiring direct database access.
- **New endpoint**: `PUT /v0/servers/{id}` for editing existing servers
- **Security**: Uses existing JWT auth with `PermissionActionEdit`
permissions
- **Anti-undelete protection**: Added `ServerStatusDeleted` status that
cannot be reverted
- **Full implementation**: Database, service, API, and comprehensive
test coverage
## Key Use Cases
- Mark malicious servers as `deleted` for takedown
- Update server metadata for moderation actions
Related to: https://github.com/modelcontextprotocol/registry/issues/182
(but that is not _done_ yet because we need admins to be able to login
(probably with #283), and need to add docs on how to do the takedown,
and probably guidelines on what we will/won't takedown)
181 lines
5.3 KiB
Go
181 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/modelcontextprotocol/registry/internal/database"
|
|
"github.com/modelcontextprotocol/registry/internal/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 ServerDetail
|
|
serverDetails := []*model.ServerDetail{
|
|
{
|
|
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.ServerDetail)
|
|
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) ([]model.ServerResponse, 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
|
|
}
|
|
|
|
// Convert ServerRecord to ServerResponse format
|
|
result := make([]model.ServerResponse, len(serverRecords))
|
|
for i, record := range serverRecords {
|
|
result[i] = record.ToServerResponse()
|
|
}
|
|
|
|
return result, nextCursor, nil
|
|
}
|
|
|
|
// GetByID retrieves a specific server by its registry metadata ID in extension wrapper format
|
|
func (s *fakeRegistryService) GetByID(id string) (*model.ServerResponse, 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
|
|
}
|
|
|
|
// Convert ServerRecord to ServerResponse format
|
|
response := serverRecord.ToServerResponse()
|
|
return &response, nil
|
|
}
|
|
|
|
// Publish publishes a server with separated extensions
|
|
func (s *fakeRegistryService) Publish(req model.PublishRequest) (*model.ServerResponse, 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 := model.ValidatePublisherExtensions(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate server name exists
|
|
if _, err := model.ParseServerName(req.Server); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract publisher extensions from request
|
|
publisherExtensions := model.ExtractPublisherExtensions(req)
|
|
|
|
// Create registry metadata for fake service (always marks as latest)
|
|
now := time.Now()
|
|
registryMetadata := model.RegistryMetadata{
|
|
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
|
|
}
|
|
|
|
// Convert ServerRecord to ServerResponse format
|
|
response := serverRecord.ToServerResponse()
|
|
return &response, nil
|
|
}
|
|
|
|
// EditServer updates an existing server with new details (admin operation)
|
|
func (s *fakeRegistryService) EditServer(id string, req model.PublishRequest) (*model.ServerResponse, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Validate the request
|
|
if err := model.ValidatePublisherExtensions(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate server name exists and format
|
|
if _, err := model.ParseServerName(req.Server); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract publisher extensions from request
|
|
publisherExtensions := model.ExtractPublisherExtensions(req)
|
|
|
|
// Update server in database
|
|
serverRecord, err := s.db.UpdateServer(ctx, id, req.Server, publisherExtensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert ServerRecord to ServerResponse format
|
|
response := serverRecord.ToServerResponse()
|
|
return &response, nil
|
|
}
|
|
|
|
// Close closes the in-memory database connection
|
|
func (s *fakeRegistryService) Close() error {
|
|
return s.db.Close()
|
|
}
|