registry/internal/service/fake_service.go
adam jones 6431f23107
feat: add admin edit endpoint for server moderation (#315)
## 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)
2025-08-27 21:28:04 +01:00

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()
}