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:
parent
2e8c761edf
commit
4fb5bc477e
14 changed files with 1702 additions and 2 deletions
15
internal/api/handlers/v0/health.go
Normal file
15
internal/api/handlers/v0/health.go
Normal 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"})
|
||||
}
|
||||
}
|
||||
27
internal/api/handlers/v0/ping.go
Normal file
27
internal/api/handlers/v0/ping.go
Normal 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)
|
||||
}
|
||||
}
|
||||
125
internal/api/handlers/v0/servers.go
Normal file
125
internal/api/handlers/v0/servers.go
Normal 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)
|
||||
}
|
||||
}
|
||||
19
internal/api/router/router.go
Normal file
19
internal/api/router/router.go
Normal 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
19
internal/api/router/v0.go
Normal 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
48
internal/api/server.go
Normal 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
56
internal/config/config.go
Normal 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,
|
||||
}
|
||||
}
|
||||
48
internal/database/database.go
Normal file
48
internal/database/database.go
Normal 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
152
internal/database/memory.go
Normal 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
189
internal/database/mongo.go
Normal 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
79
internal/model/model.go
Normal 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"`
|
||||
}
|
||||
109
internal/service/fake_service.go
Normal file
109
internal/service/fake_service.go
Normal 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()
|
||||
}
|
||||
83
internal/service/registry_service.go
Normal file
83
internal/service/registry_service.go
Normal 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 ®istryServiceImpl{
|
||||
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
|
||||
}
|
||||
9
internal/service/service.go
Normal file
9
internal/service/service.go
Normal 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)
|
||||
}
|
||||
Loading…
Reference in a new issue