Init world. Basic registry API server

Work in progress .
This commit adds the core registry service implementation with methods to:
- Retrieve all registry entries
- List entries with cursor-based pagination
- Fetch specific server details by ID
This commit is contained in:
Avinash Sridhar 2025-04-22 17:09:44 -04:00
commit 4fb5bc477e
No known key found for this signature in database
14 changed files with 1702 additions and 2 deletions

View file

@ -0,0 +1,15 @@
// Package v0 contains API handlers for version 0 of the API
package v0
import (
"encoding/json"
"net/http"
)
// HealthHandler returns a handler for health check endpoint
func HealthHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
}

View file

@ -0,0 +1,27 @@
// Package v0 contains API handlers for version 0 of the API
package v0
import (
"encoding/json"
"net/http"
"github.com/modelcontextprotocol/registry/internal/config"
)
// PingHandler returns a handler for the ping endpoint that returns build version
func PingHandler(cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
response := map[string]string{
"status": "ok",
"version": cfg.Version,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
}

View file

@ -0,0 +1,125 @@
// Package v0 contains API handlers for version 0 of the API
package v0
import (
"encoding/json"
"net/http"
"strconv"
"github.com/google/uuid"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/modelcontextprotocol/registry/internal/service"
)
// Response is a paginated API response
type PaginatedResponse struct {
Data []model.Entry `json:"servers"`
Metadata Metadata `json:"metadata,omitempty"`
}
// Metadata contains pagination metadata
type Metadata struct {
NextCursor string `json:"next_cursor,omitempty"`
}
// ServersHandler returns a handler for listing registry items
func ServersHandler(registry service.RegistryService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse cursor and limit from query parameters
cursor := r.URL.Query().Get("cursor")
if cursor != "" {
_, err := uuid.Parse(cursor)
if err != nil {
http.Error(w, "Invalid cursor parameter", http.StatusBadRequest)
return
}
}
limitStr := r.URL.Query().Get("limit")
// Default limit if not specified
limit := 30
// Try to parse limit from query param
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil {
http.Error(w, "Invalid limit parameter", http.StatusBadRequest)
return
}
// Check if limit is within reasonable bounds
if parsedLimit <= 0 {
http.Error(w, "Limit must be greater than 0", http.StatusBadRequest)
return
}
if parsedLimit > 100 {
// Cap maximum limit to prevent excessive queries
limit = 100
} else {
limit = parsedLimit
}
}
// Use the GetAll method to get paginated results
registries, nextCursor, err := registry.List(cursor, limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create paginated response
response := PaginatedResponse{
Data: registries,
}
// Add metadata if there's a next cursor
if nextCursor != "" {
response.Metadata = Metadata{
NextCursor: nextCursor,
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
}
// ServersDetailHandler returns a handler for getting details of a specific server by ID
func ServersDetailHandler(registry service.RegistryService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract the server ID from the URL path
id := r.PathValue("id")
// Validate that the ID is a valid UUID
_, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Invalid server ID format", http.StatusBadRequest)
return
}
// Get the server details from the registry service
serverDetail, err := registry.GetByID(id)
if err != nil {
if err.Error() == "record not found" {
http.Error(w, "Server not found", http.StatusNotFound)
return
}
http.Error(w, "Error retrieving server details", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(serverDetail)
}
}

View file

@ -0,0 +1,19 @@
// Package router contains API routing logic
package router
import (
"net/http"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/service"
)
// New creates a new router with all API versions registered
func New(cfg *config.Config, registry service.RegistryService) *http.ServeMux {
mux := http.NewServeMux()
// Register routes for all API versions
RegisterV0Routes(mux, cfg, registry)
return mux
}

19
internal/api/router/v0.go Normal file
View file

@ -0,0 +1,19 @@
// Package router contains API routing logic
package router
import (
"net/http"
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/service"
)
// RegisterV0Routes registers all v0 API routes to the provided router
func RegisterV0Routes(mux *http.ServeMux, cfg *config.Config, registry service.RegistryService) {
// Register v0 endpoints
mux.HandleFunc("/v0/health", v0.HealthHandler())
mux.HandleFunc("/v0/servers", v0.ServersHandler(registry))
mux.HandleFunc("/v0/servers/{id}", v0.ServersDetailHandler(registry))
mux.HandleFunc("/v0/ping", v0.PingHandler(cfg))
}

48
internal/api/server.go Normal file
View file

@ -0,0 +1,48 @@
package api
import (
"context"
"log"
"net/http"
"github.com/modelcontextprotocol/registry/internal/api/router"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/service"
)
// Server represents the HTTP server
type Server struct {
config *config.Config
registry service.RegistryService
router *http.ServeMux
server *http.Server
}
// NewServer creates a new HTTP server
func NewServer(cfg *config.Config, registryService service.RegistryService) *Server {
// Create router with all API versions registered
mux := router.New(cfg, registryService)
server := &Server{
config: cfg,
registry: registryService,
router: mux,
server: &http.Server{
Addr: cfg.ServerAddress,
Handler: mux,
},
}
return server
}
// Start begins listening for incoming HTTP requests
func (s *Server) Start() error {
log.Printf("HTTP server starting on %s", s.config.ServerAddress)
return s.server.ListenAndServe()
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
return s.server.Shutdown(ctx)
}

56
internal/config/config.go Normal file
View file

@ -0,0 +1,56 @@
package config
import "os"
// Config holds the application configuration
type Config struct {
ServerAddress string
DatabaseURL string
DatabaseName string
CollectionName string
LogLevel string
Environment string // Added environment field
Version string // Application version
}
// NewConfig creates a new configuration with default values
func NewConfig() *Config {
// Get environment from OS environment variable or default to "production"
env := os.Getenv("APP_ENV")
if env == "" {
env = "production"
}
// Get version from environment variable or default to "dev"
version := os.Getenv("APP_VERSION")
if version == "" {
version = "dev"
}
// Get database URL from environment variable or default to localhost
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "localhost:5432"
}
// Get database name from environment variable or default to "mcp_registry"
dbName := os.Getenv("DATABASE_NAME")
if dbName == "" {
dbName = "mcp-registry"
}
// Get collection name from environment variable or default to "entries"
collectionName := os.Getenv("COLLECTION_NAME")
if collectionName == "" {
collectionName = "servers_v2"
}
return &Config{
ServerAddress: ":8080",
DatabaseURL: dbURL,
DatabaseName: dbName,
CollectionName: collectionName,
LogLevel: "info",
Environment: env,
Version: version,
}
}

View file

@ -0,0 +1,48 @@
package database
import (
"context"
"errors"
"github.com/modelcontextprotocol/registry/internal/model"
)
// Common database errors
var (
ErrNotFound = errors.New("record not found")
ErrAlreadyExists = errors.New("record already exists")
ErrInvalidInput = errors.New("invalid input")
ErrDatabase = errors.New("database error")
)
// Database defines the interface for database operations on MCPRegistry entries
type Database interface {
// List retrieves all MCPRegistry entries with optional filtering
List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, string, error)
// GetByID retrieves a single ServerDetail by it's ID
GetByID(ctx context.Context, id string) (*model.ServerDetail, error)
// Close closes the database connection
Close() error
}
// ConnectionType represents the type of database connection
type ConnectionType string
const (
// ConnectionTypeMemory represents an in-memory database connection
ConnectionTypeMemory ConnectionType = "memory"
// ConnectionTypeMongoDB represents a MongoDB database connection
ConnectionTypeMongoDB ConnectionType = "mongodb"
)
// ConnectionInfo provides information about the database connection
type ConnectionInfo struct {
// Type indicates the type of database connection
Type ConnectionType
// IsConnected indicates whether the database is currently connected
IsConnected bool
// Raw provides access to the underlying connection object, which will vary by implementation
// For MongoDB, this will be *mongo.Client
// For MemoryDB, this will be map[string]*model.MCPRegistry
Raw any
}

152
internal/database/memory.go Normal file
View file

@ -0,0 +1,152 @@
package database
import (
"context"
"sort"
"sync"
"github.com/modelcontextprotocol/registry/internal/model"
)
// MemoryDB is an in-memory implementation of the Database interface
type MemoryDB struct {
entries map[string]*model.Entry
mu sync.RWMutex
}
// NewMemoryDB creates a new instance of the in-memory database
func NewMemoryDB(e map[string]*model.Entry) *MemoryDB {
return &MemoryDB{
entries: e,
}
}
// List retrieves all MCPRegistry entries with optional filtering and pagination
func (db *MemoryDB) List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, string, error) {
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
if limit <= 0 {
limit = 10 // Default limit
}
db.mu.RLock()
defer db.mu.RUnlock()
// Convert all entries to a slice for pagination
var allEntries []*model.Entry
for _, entry := range db.entries {
entryCopy := *entry
allEntries = append(allEntries, &entryCopy)
}
// Simple filtering implementation
var filteredEntries []*model.Entry
for _, entry := range allEntries {
include := true
// Apply filters if any
if filter != nil {
for key, value := range filter {
switch key {
case "name":
if entry.Name != value.(string) {
include = false
}
case "repoUrl":
if entry.Repository.URL != value.(string) {
include = false
}
case "serverDetail.id":
if entry.ID != value.(string) {
include = false
}
case "version":
if entry.Version != value.(string) {
include = false
}
// Add more filter options as needed
}
}
}
if include {
filteredEntries = append(filteredEntries, entry)
}
}
// Find starting point for cursor-based pagination
startIdx := 0
if cursor != "" {
for i, entry := range filteredEntries {
if entry.ID == cursor {
startIdx = i + 1 // Start after the cursor
break
}
}
}
// Sort filteredEntries by ID for consistent pagination
sort.Slice(filteredEntries, func(i, j int) bool {
return filteredEntries[i].ID < filteredEntries[j].ID
})
// Apply pagination
endIdx := startIdx + limit
if endIdx > len(filteredEntries) {
endIdx = len(filteredEntries)
}
var result []*model.Entry
if startIdx < len(filteredEntries) {
result = filteredEntries[startIdx:endIdx]
} else {
result = []*model.Entry{}
}
// Determine next cursor
nextCursor := ""
if endIdx < len(filteredEntries) {
nextCursor = filteredEntries[endIdx-1].ID
}
return result, nextCursor, nil
}
// GetByID retrieves a single ServerDetail by its ID
func (db *MemoryDB) GetByID(ctx context.Context, id string) (*model.ServerDetail, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
db.mu.RLock()
defer db.mu.RUnlock()
if entry, exists := db.entries[id]; exists {
return &model.ServerDetail{
ID: entry.ID,
Name: entry.Name,
Description: entry.Description,
Version: entry.Version,
Repository: entry.Repository,
}, nil
}
return nil, ErrNotFound
}
// Close closes the database connection
// For an in-memory database, this is a no-op
func (db *MemoryDB) Close() error {
return nil
}
// Connection returns information about the database connection
func (db *MemoryDB) Connection() *ConnectionInfo {
return &ConnectionInfo{
Type: ConnectionTypeMemory,
IsConnected: true, // Memory DB is always connected
Raw: db.entries,
}
}

189
internal/database/mongo.go Normal file
View file

@ -0,0 +1,189 @@
package database
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/modelcontextprotocol/registry/internal/model"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// MongoDB is an implementation of the Database interface using MongoDB
type MongoDB struct {
client *mongo.Client
database *mongo.Database
collection *mongo.Collection
}
// NewMongoDB creates a new instance of the MongoDB database
func NewMongoDB(ctx context.Context, connectionURI, databaseName, collectionName string) (*MongoDB, error) {
// Set client options and connect to MongoDB
clientOptions := options.Client().ApplyURI(connectionURI)
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
return nil, err
}
// Ping the MongoDB server to verify the connection
if err = client.Ping(ctx, nil); err != nil {
return nil, err
}
// Get database and collection
database := client.Database(databaseName)
collection := database.Collection(collectionName)
// Create indexes for better query performance
models := []mongo.IndexModel{
{
Keys: bson.D{{Key: "name", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "id", Value: 1}},
Options: options.Index().SetUnique(true),
},
}
_, err = collection.Indexes().CreateMany(ctx, models)
if err != nil {
return nil, err
}
return &MongoDB{
client: client,
database: database,
collection: collection,
}, nil
}
// List retrieves MCPRegistry entries with optional filtering and pagination
func (db *MongoDB) List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, string, error) {
if limit <= 0 {
// Set default limit if not provided
limit = 10
}
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
// Convert Go map to MongoDB filter
mongoFilter := bson.M{}
// Map common filter keys to MongoDB document paths
for k, v := range filter {
// Handle nested fields with dot notation
switch k {
case "publisher.trusted":
mongoFilter["publisher.trusted"] = v
default:
mongoFilter[k] = v
}
}
// Setup pagination options
findOptions := options.Find()
// If cursor is provided, add condition to filter to only get records after the cursor
if cursor != "" {
// Validate that the cursor is a valid UUID
if _, err := uuid.Parse(cursor); err != nil {
return nil, "", fmt.Errorf("invalid cursor format: %w", err)
}
// Fetch the document at the cursor to get its sort values
var cursorDoc model.Entry
err := db.collection.FindOne(ctx, bson.M{"id": cursor}).Decode(&cursorDoc)
if err != nil {
if err == mongo.ErrNoDocuments {
// If cursor document not found, start from beginning
} else {
return nil, "", err
}
} else {
// Use the cursor document's ID to paginate (records with ID > cursor's ID)
mongoFilter["id"] = bson.M{"$gt": cursor}
}
}
// Set sort order by ID (for consistent pagination)
findOptions.SetSort(bson.M{"id": 1})
// Set limit if provided and valid
if limit > 0 {
findOptions.SetLimit(int64(limit))
}
// Execute find operation with options
mongoCursor, err := db.collection.Find(ctx, mongoFilter, findOptions)
if err != nil {
return nil, "", err
}
defer mongoCursor.Close(ctx)
// Decode results
var results []*model.Entry
if err = mongoCursor.All(ctx, &results); err != nil {
return nil, "", err
}
// Determine the next cursor
nextCursor := ""
if len(results) > 0 && limit > 0 && len(results) >= limit {
// Use the last item's ID as the next cursor
nextCursor = results[len(results)-1].ID
}
return results, nextCursor, nil
}
// GetByID retrieves a single ServerDetail by its ID
func (db *MongoDB) GetByID(ctx context.Context, id string) (*model.ServerDetail, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Create a filter for the ID
filter := bson.M{"id": id}
// Find the entry in the database
var entry model.ServerDetail
err := db.collection.FindOne(ctx, filter).Decode(&entry)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, ErrNotFound
}
return nil, fmt.Errorf("error retrieving entry: %w", err)
}
// Create and return a ServerDetail from the entry data
return &entry, nil
}
// Close closes the database connection
func (db *MongoDB) Close() error {
return db.client.Disconnect(context.Background())
}
// Connection returns information about the database connection
func (db *MongoDB) Connection() *ConnectionInfo {
isConnected := false
// Check if the client is connected
if db.client != nil {
// A quick ping with 1 second timeout to verify connection
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := db.client.Ping(ctx, nil)
isConnected = (err == nil)
}
return &ConnectionInfo{
Type: ConnectionTypeMongoDB,
IsConnected: isConnected,
Raw: db.client,
}
}

79
internal/model/model.go Normal file
View file

@ -0,0 +1,79 @@
package model
type Entry struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Repository Repository `json:"repository,omitempty"`
Version string `json:"version,omitempty"`
}
type Repository struct {
URL string `json:"url,omitempty"`
SubFolder string `json:"subfolder,omitempty"`
Branch string `json:"branch,omitempty"`
Commit string `json:"commit,omitempty"`
}
type ServerDetail struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
Repository Repository `json:"repository,omitempty"`
RegistryCanonical string `json:"registry_canonical,omitempty"`
Registries []Registries `json:"registries,omitempty"`
Remotes []Remotes `json:"remotes,omitempty"`
}
type Registries struct {
Name string `json:"name,omitempty"`
PackageName string `json:"package_name,omitempty"`
License string `json:"license,omitempty"`
CommandArguments Command `json:"command_arguments,omitempty"`
}
type Remotes struct {
TransportType string `json:"transport_type,omitempty"`
Url string `json:"url,omitempty"`
}
type Command struct {
SubCommands []SubCommand `json:"sub_commands,omitempty"`
PositionalArguments []PositionalArgument `json:"positional_arguments,omitempty"`
EnvironmentVariables []EnvironmentVariable `json:"environment_variables,omitempty"`
NamedArguments []NamedArguments `json:"named_arguments,omitempty"`
}
type EnvironmentVariable struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Required bool `json:"required,omitempty"`
}
type Argument struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
DefaultValue string `json:"default_value,omitempty"`
IsRequired bool `json:"is_required,omitempty"`
IsEditable bool `json:"is_editable,omitempty"`
IsRepeatable bool `json:"is_repeatable,omitempty"`
Choices []string `json:"choices,omitempty"`
}
type PositionalArgument struct {
Position int `json:"position,omitempty"`
Argument Argument `json:"argument,omitempty"`
}
type SubCommand struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
NamedArguments []NamedArguments `json:"named_arguments,omitempty"`
}
type NamedArguments struct {
ShortFlag string `json:"short_flag,omitempty"`
LongFlag string `json:"long_flag,omitempty"`
RequiresValue bool `json:"requires_value,omitempty"`
Argument Argument `json:"argument,omitempty"`
}

View file

@ -0,0 +1,109 @@
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
registries := []*model.Entry{
{
ID: uuid.New().String(),
Name: "bluegreen/mcp-server",
Description: "A dummy MCP registry for testing",
Repository: model.Repository{
URL: "https://github.com/example/mcp-1",
SubFolder: "server",
Branch: "main",
Commit: "abc123def456",
},
Version: "1.0.0",
},
{
ID: uuid.New().String(),
Name: "orangepurple/mcp-server",
Description: "Another dummy MCP registry for testing",
Repository: model.Repository{
URL: "https://github.com/example/mcp-2",
SubFolder: "mcp",
Branch: "develop",
Commit: "def456ghi789",
},
Version: "2.1.0",
},
{
ID: uuid.New().String(),
Name: "greenyellow/mcp-server",
Description: "Yet another dummy MCP registry for testing",
Repository: model.Repository{
URL: "https://github.com/example/mcp-3",
SubFolder: "",
Branch: "feature/mcp-server",
Commit: "789jkl012mno",
},
Version: "0.9.5",
},
}
// Create a new in-memory database
registryMap := make(map[string]*model.Entry)
for _, entry := range registries {
registryMap[entry.ID] = entry
}
memDB := database.NewMemoryDB(registryMap)
return &fakeRegistryService{
db: memDB,
}
}
// List retrieves MCPRegistry entries with optional filtering and pagination
func (s *fakeRegistryService) List(cursor string, limit int) ([]model.Entry, 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 with no filters to get all entries
entries, nextCursor, err := s.db.List(ctx, nil, cursor, limit)
if err != nil {
return nil, "", err
}
// Convert from []*model.Entry to []model.Entry
result := make([]model.Entry, len(entries))
for i, entry := range entries {
result[i] = *entry
}
return result, nextCursor, nil
}
// GetByID retrieves a specific server detail by its ID
func (s *fakeRegistryService) GetByID(id string) (*model.ServerDetail, 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 detail
serverDetail, err := s.db.GetByID(ctx, id)
if err != nil {
return nil, err
}
return serverDetail, nil
}
// Close closes the in-memory database connection
func (s *fakeRegistryService) Close() error {
return s.db.Close()
}

View file

@ -0,0 +1,83 @@
package service
import (
"context"
"time"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/model"
)
// registryServiceImpl implements the RegistryService interface using our Database
type registryServiceImpl struct {
db database.Database
}
// NewRegistryServiceWithDB creates a new registry service with the provided database
func NewRegistryServiceWithDB(db database.Database) RegistryService {
return &registryServiceImpl{
db: db,
}
}
// GetAll returns all registry entries
func (s *registryServiceImpl) GetAll() ([]model.Entry, 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 with no filters to get all entries
entries, _, err := s.db.List(ctx, nil, "", 30)
if err != nil {
return nil, err
}
// Convert from []*model.Entry to []model.Entry
result := make([]model.Entry, len(entries))
for i, entry := range entries {
result[i] = *entry
}
return result, nil
}
// List returns registry entries with cursor-based pagination
func (s *registryServiceImpl) List(cursor string, limit int) ([]model.Entry, string, error) {
// Create a timeout context for the database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// If limit is not set or negative, use a default limit
if limit <= 0 {
limit = 30
}
// Use the database's List method with pagination
entries, nextCursor, err := s.db.List(ctx, nil, cursor, limit)
if err != nil {
return nil, "", err
}
// Convert from []*model.MCPRegistry to []model.MCPRegistry
result := make([]model.Entry, len(entries))
for i, entry := range entries {
result[i] = *entry
}
return result, nextCursor, nil
}
// GetByID retrieves a specific server detail by its ID
func (s *registryServiceImpl) GetByID(id string) (*model.ServerDetail, 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 detail
serverDetail, err := s.db.GetByID(ctx, id)
if err != nil {
return nil, err
}
return serverDetail, nil
}

View file

@ -0,0 +1,9 @@
package service
import "github.com/modelcontextprotocol/registry/internal/model"
// RegistryService defines the interface for registry operations
type RegistryService interface {
List(cursor string, limit int) ([]model.Entry, string, error)
GetByID(id string) (*model.ServerDetail, error)
}